@naanlang/naan 1.4.3 → 1.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 (50) hide show
  1. package/LICENSE.md +2 -2
  2. package/README.md +4 -4
  3. package/bin/index.js +22 -3
  4. package/dist/env_web.js +2 -2
  5. package/dist/naan.min.js +6 -6
  6. package/frameworks/browser/browser.nlg +2 -2
  7. package/frameworks/browser/https_request.nlg +39 -10
  8. package/frameworks/browser/sworker.js +23 -23
  9. package/frameworks/browser/terminals.nlg +33 -24
  10. package/frameworks/client/apiclient.nlg +31 -18
  11. package/frameworks/client/client.nlg +22 -2
  12. package/frameworks/client/psm_client.nlg +12 -3
  13. package/frameworks/node/filesystem.nlg +9 -7
  14. package/frameworks/node/gitter.nlg +9 -7
  15. package/frameworks/node/http_request.nlg +103 -0
  16. package/frameworks/node/https_request.nlg +42 -9
  17. package/frameworks/node/node.nlg +1 -0
  18. package/frameworks/node/shell.nlg +4 -4
  19. package/frameworks/node/worker.nlg +1 -0
  20. package/frameworks/project/projects.nlg +2 -1
  21. package/frameworks/running/executors.nlg +26 -7
  22. package/frameworks/running/taskexec.nlg +48 -14
  23. package/frameworks/storage/dbt_pouch.nlg +26 -13
  24. package/frameworks/storage/psm.nlg +39 -5
  25. package/frameworks/storage/psm_dbtables.nlg +10 -2
  26. package/lib/browser/env_web.js +9 -9
  27. package/lib/browser/env_webworker.js +1 -2
  28. package/lib/core/naanlib.js +6 -6
  29. package/lib/env_node.js +9 -3
  30. package/lib/node_repl_init.nlg +2 -2
  31. package/package.json +1 -1
  32. package/plugins/nodeLib/node_util.nlg +14 -7
  33. package/plugins/openSearch/os_dynamo.nlg +5 -5
  34. package/plugins/serviceAws/aws_cloudwatchlogs.nlg +2 -1
  35. package/plugins/serviceAws/aws_dynamo.nlg +26 -8
  36. package/plugins/serviceAws/aws_s3.nlg +3 -3
  37. package/plugins/serviceAws/aws_sqs.nlg +3 -1
  38. package/plugins/serviceAws/aws_utils.nlg +3 -1
  39. package/plugins/serviceAws/aws_vm.nlg +198 -0
  40. package/plugins/serviceAws/dbt_aws.nlg +22 -20
  41. package/plugins/serviceAws/psm_aws.nlg +2 -1
  42. package/plugins/serviceGC/gc_api.nlg +1 -0
  43. package/plugins/serviceNexara/serviceNexara.nlg +32 -0
  44. package/plugins/serviceNexara/speechrec.nlg +160 -0
  45. package/plugins/serviceYC/serviceYC.nlg +2 -3
  46. package/plugins/serviceYC/yc_speechrec.nlg +4 -3
  47. package/plugins/serviceYC/yc_vm.nlg +44 -54
  48. package/test/test_01_core.nlg +42 -33
  49. package/test/test_03_jsinterop.nlg +35 -6
  50. package/plugins/serviceGC/russian_trusted_root_ca_pem.crt +0 -33
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * column positioning: // // !
8
8
  *
9
- * Copyright (c) 2020-2021 by Richard C. Zulch
9
+ * Copyright (c) 2020-2025 by Richard C. Zulch
10
10
  *
11
11
  */
12
12
 
@@ -56,7 +56,7 @@ closure Gitter(fs, local gitt) {
56
56
  asyncResult(callback, false, gitt.roots[repopath])
57
57
  execOptions = {
58
58
  cmdargs: ["rev-parse", "--show-toplevel"]
59
- execops: {
59
+ spawnops: {
60
60
  cwd: repopath
61
61
  }
62
62
  outLE: "\x00" // lines delimited by nulls
@@ -117,7 +117,7 @@ closure Gitter(fs, local gitt) {
117
117
  repopath = fs.path.resolve(fs.rootpath, repopath)
118
118
  execOptions = {
119
119
  cmdargs: ["log", "-z", "--pretty=format:%H|%an|%ae|%aI|%B"]
120
- execops: {
120
+ spawnops: {
121
121
  cwd: repopath
122
122
  }
123
123
  outLE: "\x00" // lines delimited by nulls
@@ -168,10 +168,12 @@ closure Gitter(fs, local gitt) {
168
168
  if !string(repopath)
169
169
  return (asyncResult(callback, Error("invalid path:", repopath)))
170
170
  repopath = fs.path.resolve(fs.rootpath, repopath)
171
- bridge = bridgePath(repopath)
171
+ bridge = fs.path.normalize(bridgePath(repopath)) // ensure it works for our FS
172
+ if bridge == "."
173
+ bridge = "" // no bridge in this case
172
174
  execOptions = {
173
175
  cmdargs: ["status", "-z", "--porcelain"]
174
- execops: {
176
+ spawnops: {
175
177
  cwd: repopath
176
178
  }
177
179
  outLE: "\x00" // lines delimited by nulls
@@ -219,13 +221,13 @@ closure Gitter(fs, local gitt) {
219
221
  repopath = fs.path.resolve(fs.rootpath, repopath)
220
222
  execOptions = {
221
223
  cmdargs: ["show", options.hash.concat(":", bridgePath(repopath), gitPath(options.pathspec))]
222
- execops: {
224
+ spawnops: {
223
225
  cwd: repopath
224
226
  }
225
227
  }
226
228
  nodecp.execFile("git",
227
229
  execOptions.cmdargs,
228
- execOptions.execops,
230
+ execOptions.spawnops,
229
231
  function(err, stdout, stderr) {
230
232
  if err
231
233
  callback(Error("git failed", err, stderr))
@@ -0,0 +1,103 @@
1
+ /*
2
+ * http_request.nlg
3
+ *
4
+ * Http operations for NodeJS.
5
+ *
6
+ * column positioning: // // !
7
+ *
8
+ * Copyright (c) 2024-2025 by Richard C. Zulch
9
+ *
10
+ */
11
+
12
+
13
+ /* HttpRequest
14
+ *
15
+ * Make a simple http request. This is not supported by the standard NaaN framework HttpsApiRequest
16
+ * because it is not SSL.
17
+ *
18
+ * Options:
19
+ * {
20
+ * method: <string> // HTTP method (defaults to GET)
21
+ * headers: <dictionary> // key/data pairs
22
+ * putdata: <data> // data to put
23
+ * json: <boolean> // ignore content-type of return and decode JSON
24
+ * }
25
+ *
26
+ * The return is a result tuple comprising:
27
+ * (<error>, -- false on success
28
+ * <content>, -- returned data
29
+ * {
30
+ * status: <integer> -- http response status
31
+ * headers: <array> -- headers if available
32
+ * })
33
+ *
34
+ */
35
+
36
+ closure HttpRequest(url, options,
37
+ local pending, chunks, params, request) {
38
+ global(js, nodeHttp)
39
+ pending = new(nonce)
40
+ chunks = []
41
+ params = {
42
+ method: options.method || undefined
43
+ headers: options.headers || undefined
44
+ }
45
+ request = nodeHttp.request(url, params, closure (response) {
46
+ response.on("data", function (chunk) {
47
+ chunks.push(chunk)
48
+ })
49
+ response.on("end", function (local content, error) {
50
+ content = js.g.Buffer.concat(chunks)
51
+ if response.statusCode < 200 || response.statusCode >= 299 { // 299 is "cancelled"
52
+ debuglog("httpRequest status:", url, response.statusCode, "message:", content)
53
+ pending.signal(list(Error("httpRequest statusCode:", url, response.statusCode, {
54
+ status: response.statusCode
55
+ }), false, { status: response.statusCode }))
56
+ return
57
+ }
58
+ if response.complete {
59
+ content = content.toString()
60
+ if options.json
61
+ `(error, content) = JsonParse(content)
62
+ }
63
+ else
64
+ error = Error("httpRequest terminated prematurely:", url, response.statusCode)
65
+ pending.signal(list(error, content, {
66
+ headers: response.headers
67
+ status: response.statusCode
68
+ contentType: response.headers["content-type"]
69
+ }))
70
+ })
71
+ })
72
+ request.on("error", function hre(error) {
73
+ debuglog("httpRequest error:", url, "error:", error)
74
+ error = Error("httpRequest failed:", url, error)
75
+ pending.signal(list(error))
76
+ })
77
+ if options.putdata {
78
+ if string(options.putdata)
79
+ request.write(options.putdata)
80
+ else
81
+ request.write(JSONstringify(options.putdata))
82
+ }
83
+ request.end()
84
+ pending.wait()
85
+ };
86
+
87
+
88
+ /*
89
+ * http_nodeInit
90
+ *
91
+ * Initialize HTTP request operations for NodeJS.
92
+ *
93
+ */
94
+
95
+ function http_nodeInit(local manifest) {
96
+ manifest = `(HttpRequest, http_nodeInit)
97
+
98
+ Naan.module.build(module.id, "http_request", function(modobj, compobj) {
99
+ require("./node.nlg")
100
+ compobj.manifest = manifest
101
+ modobj.exports.HttpRequest = HttpRequest
102
+ })
103
+ }();
@@ -47,7 +47,8 @@
47
47
  */
48
48
 
49
49
  closure httpsApiReqOnce(url, options,
50
- local urlq, reqOptions, putdata, item, xurl, pending, chunks, request) {
50
+ local urlq, reqOptions, putdata, error, item, xurl, pending, chunks, request) {
51
+ global(js, nodeHttps, nodeUrl)
51
52
  if options.query
52
53
  urlq = url.concat(EncodeQuery("?", options.query))
53
54
  else
@@ -126,7 +127,10 @@ closure httpsApiReqOnce(url, options,
126
127
  debuglog("HttpsApiRequest status:", url, response.statusCode, "message:", content)
127
128
  pending.signal(list(Error("HttpsApiRequest statusCode:", url, response.statusCode, {
128
129
  status: response.statusCode
129
- }), false, { status: response.statusCode }))
130
+ }), false, {
131
+ headers: response.headers
132
+ status: response.statusCode
133
+ }))
130
134
  return
131
135
  }
132
136
  if response.complete {
@@ -234,28 +238,56 @@ closure sseParserFactory(message, local parser) {
234
238
  *
235
239
  */
236
240
 
237
- closure HttpsApiRequest(url, options, local parser, decoder, result) {
241
+ closure HttpsApiRequest(url, options,
242
+ local delayf, parser, decoder, error, content, status) {
243
+ global(stringDecoder, common)
244
+ //
245
+ // defRetrier -- backoff and retry selected errors
246
+ //
247
+ function defRetrier(error, headers, local retryAfter, retryms, waitms) {
248
+ if member(error.status, `(408, 500, 502, 503, 504)) { // retriable errors
249
+ retryAfter = headers["retry-after"]
250
+ if retryAfter {
251
+ retryms = toint(retryAfter) * 1000
252
+ if !retryms
253
+ retryms = Date(retryAfter).getTime() - Date.now()
254
+ if retryms < 0
255
+ retryms = false
256
+ }
257
+ if !delayf
258
+ delayf = common.ExbackFactory(12,30) // 12s max backoff, 30s deadline
259
+ waitms = delayf()
260
+ if !waitms || waitms < retryms
261
+ waitms = retryms
262
+ if options.debug
263
+ debuglog("HttpsApiRequest.defRetrier: wait ${waitms} msec using retry-after ${retryAfter}")
264
+ if waitms // return false to give up
265
+ sleep(waitms) // return true from sleep to retry
266
+ }
267
+ }
268
+
269
+ options = merge({ retrier: defRetrier }, options)
238
270
  if options.sseHook {
239
271
  parser = sseParserFactory(options.sseHook)
240
272
  decoder = xnew(stringDecoder.StringDecoder, "utf8")
241
- options = merge(options, {
273
+ options = merge({
242
274
  streamHook: function(chunk) {
243
275
  if chunk
244
276
  parser.write(decoder.write(chunk)) // data arrived
245
277
  else
246
278
  parser.write(decoder.end()) // end of data
247
279
  }
248
- })
280
+ }, options)
249
281
  }
250
282
  loop {
251
- result = httpsApiReqOnce(url, options)
252
- if result.0 && result.0.status { // check for retries
253
- if !options.retrier(result.0)
283
+ `(error, content, status) = httpsApiReqOnce(url, options)
284
+ if error && error.status { // check for retries
285
+ if !options.retrier(error, status.headers)
254
286
  break
255
287
  } else
256
288
  break
257
289
  }
258
- result
290
+ list(error, content, status)
259
291
  };
260
292
 
261
293
 
@@ -271,6 +303,7 @@ function https_nodeInit(local manifest) {
271
303
 
272
304
  Naan.module.build(module.id, "https_request", function(modobj, compobj) {
273
305
  require("./node.nlg")
306
+ common = require("../common/utils.nlg")
274
307
  compobj.manifest = manifest
275
308
  modobj.exports.HttpsApiRequest = HttpsApiRequest
276
309
  })
@@ -131,6 +131,7 @@ function nodrInit(local manifest) {
131
131
  os = js.r("os")
132
132
  stream = js.r("stream")
133
133
  nodeHttps = js.r("https")
134
+ nodeHttp = js.r("http")
134
135
  nodeUrl= js.r("url")
135
136
  threads = js.r("worker_threads")
136
137
  stringDecoder = js.r("string_decoder")
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * column positioning: // // !
8
8
  *
9
- * Copyright (c) 2024 by Richard C. Zulch
9
+ * Copyright (c) 2024-2025 by Richard C. Zulch
10
10
  *
11
11
  */
12
12
 
@@ -68,9 +68,9 @@ closure ShellConnect(options, local shob, error, fs, xparser, shelly) {
68
68
  }
69
69
  }
70
70
  if cwd
71
- xlparams.execops = { cwd: cwd }
71
+ xlparams.spawnops = { cwd: cwd }
72
72
  else if options.cwd
73
- xlparams.execops = { cwd: options.cwd }
73
+ xlparams.spawnops = { cwd: options.cwd }
74
74
  fs.execLines(cmd, xlparams, function(error, result) {
75
75
  evalactive(list(`assign, `(compress, "$$"), report)) // assign to $$ in active namespace
76
76
  pending.signal(list(error, merge(result, report)))
@@ -80,7 +80,7 @@ closure ShellConnect(options, local shob, error, fs, xparser, shelly) {
80
80
 
81
81
  // shellparser
82
82
  // intercept command-line parsing and look for a !shell escape
83
- function shellparser(source, grammar, operators, local token) {
83
+ function shellparser(source, grammar, operators, local token, text) {
84
84
  token = source.tokenread()
85
85
  if token.atom == `! {
86
86
  source.tokenlast(true) // "consume" the ! token
@@ -642,6 +642,7 @@ closure workController(api, local workco) {
642
642
  name: config.name
643
643
  workerID: workerID
644
644
  }) }
645
+ list(false, worker)
645
646
  }
646
647
 
647
648
  // wsReceive
@@ -296,7 +296,7 @@ closure projConnector(projman, projtype, local connector, watch) {
296
296
  *
297
297
  * Nide.proj/
298
298
  * nide_cliser.cfg - JSON definition of the project (see below)
299
- * NaanIDE_version.txt - [default] substitution file defining 1.4.3+1 etc.
299
+ * NaanIDE_version.txt - [default] substitution file defining 1.5.0+1 etc.
300
300
  * NaanIDE_version_builds.txt - [default] contains current build number as decimal string
301
301
  * build/ - [optional] default build output location
302
302
  * NaanIDE.lic - [optional] defines Zulch Laboratories, Inc. and other license info
@@ -692,6 +692,7 @@ closure projConfig(projman, where, projID, local procon, fs, configpath) {
692
692
  erase: true
693
693
  overwrite: true
694
694
  follow: true
695
+ cacheControl: publish.cacheControlFiles
695
696
  })
696
697
  }
697
698
 
@@ -22,6 +22,7 @@ closure ExecutorBase(track, type, name, local exec) {
22
22
  exec = new(object, this)
23
23
  exec.type = type
24
24
  exec.name = name
25
+ exec.contexts = { }
25
26
 
26
27
  // execReset
27
28
  //
@@ -57,6 +58,18 @@ closure ExecutorBase(track, type, name, local exec) {
57
58
  // The specified message was received from the instance, so signal it.
58
59
  //
59
60
  exec.execReceived = closure execReceived(data, local pending) {
61
+ if data.reverse {
62
+ if !exec.contexts[data.contextID]
63
+ return // ignore unknown context
64
+ if !exec.reverseDispatch
65
+ exec.reverseDispatch = require("./taskexec.nlg").TaskDispatcher(function(data) {
66
+ data.reverse = true
67
+ exec.PostMessage(data)
68
+ }, exec.contexts[data.contextID].coder)
69
+ data.reverse = false
70
+ exec.reverseDispatch(data)
71
+ return
72
+ }
60
73
  pending = exec.pending[data.xid]
61
74
  if !pending { // not waiting on message
62
75
  if data.xmsg == "xready"
@@ -110,7 +123,7 @@ closure ExecutorBase(track, type, name, local exec) {
110
123
  // persistent reconnection. Currently this does not ever reclaim old contexts on the instance
111
124
  // end of things, but it should. ###
112
125
  //
113
- exec.context = closure context(state, local error, retries, ms, delta, excon) {
126
+ exec.context = closure context(state, coder, local error, retries, ms, delta, excon) {
114
127
  retries = 0
115
128
  ms = milliseconds()
116
129
  delta = 1
@@ -127,8 +140,14 @@ closure ExecutorBase(track, type, name, local exec) {
127
140
  sleep(delta)
128
141
  if delta < 100
129
142
  delta *= 2 }
130
- excon = ExecutorContext(exec, state)
131
- excon.register()
143
+ excon = ExecutorContext(exec, state, coder)
144
+ `(error) = excon.register()
145
+ if error
146
+ list(error)
147
+ else {
148
+ exec.contexts[excon.state.contextID] = excon
149
+ list(false, excon)
150
+ }
132
151
  }
133
152
 
134
153
  // attention
@@ -160,7 +179,7 @@ closure ExecutorBase(track, type, name, local exec) {
160
179
  *
161
180
  */
162
181
 
163
- closure ExecutorContext(exec, state, local context, coder, util) {
182
+ closure ExecutorContext(exec, state, coder, local context, util) {
164
183
  global(apply)
165
184
  context = new(object, this)
166
185
  if !state || !state.contextID
@@ -168,7 +187,7 @@ closure ExecutorContext(exec, state, local context, coder, util) {
168
187
  contextID: UUID()
169
188
  }
170
189
  context.state = state
171
- coder = packageCoder(evalf)
190
+ context.coder = coder || packageCoder(evalf)
172
191
  util = modlist().Util.exports
173
192
 
174
193
  // getState
@@ -208,7 +227,7 @@ closure ExecutorContext(exec, state, local context, coder, util) {
208
227
  pkg = util.pkgSave(`expr, roots, {
209
228
  nsignore: true
210
229
  comments: true
211
- objectEncoder: coder.encodeObject
230
+ objectEncoder: context.coder.encodeObject
212
231
  })
213
232
  data = {
214
233
  contextID: state.contextID
@@ -224,7 +243,7 @@ closure ExecutorContext(exec, state, local context, coder, util) {
224
243
  localroots: false
225
244
  intern: "symbols"
226
245
  execute: false
227
- objectDecoder: coder.decodeObject
246
+ objectDecoder: context.coder.decodeObject
228
247
  })))
229
248
  else
230
249
  result
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * column positioning: // // !
7
7
  *
8
- * Copyright (c) 2021-2024 by Richard C. Zulch
8
+ * Copyright (c) 2021-2025 by Richard C. Zulch
9
9
  *
10
10
  */
11
11
 
@@ -19,27 +19,57 @@
19
19
  *
20
20
  */
21
21
 
22
- closure TaskDispatcher(replier, local contexts, gohome, coder, util) {
22
+ closure TaskDispatcher(replier, coder, local contexts, gohome, util) {
23
23
  global()
24
24
  contexts = { }
25
25
  gohome = makeActivator()
26
- coder = packageCoder(evalf)
27
26
  util = modlist().Util.exports
28
-
29
- // sendEval
27
+
28
+ // ContextCoder
30
29
  //
31
- // Send an evaluation request to the original execution context.
30
+ // A context-specific coder allowing remote-initiated communication.
32
31
  //
33
- closure evalf(expr) {
34
- debuglog("TaskDispatcher doesn't yet implement reverse proxy objects")
35
- list(Error("### reverse object proxy not implemented"))
32
+ closure ContextCoder(contextID, _guid, dynreply, local exec, error, context) {
33
+ contexts[contextID].coder = packageCoder(closure evalf(expr) {
34
+ if !exec {
35
+ exec = require("./executors.nlg").ExecutorBase(false, "reverser", "TD")
36
+ exec.PostMessage = closure(data) {
37
+ if data.xmsg == "xstart" { // report ready locally
38
+ data.xmsg = "xready"
39
+ exec.execReceived(data)
40
+ } else {
41
+ data.reverse = true
42
+ data._guid = _guid || undefined
43
+ dynreply(data)
44
+ }
45
+ list(false)
46
+ }
47
+ contexts[contextID].execReceived = exec.execReceived // needed for registered reply
48
+ `(error, context) = exec.context({
49
+ contextID: contextID
50
+ }, contexts[contextID].coder)
51
+ if error {
52
+ ErrorDebuglog("TaskDispatcher.ContextCoder failed:", error)
53
+ exec = false
54
+ return (list(error))
55
+ }
56
+ }
57
+ context.eval(expr)
58
+ })
36
59
  }
37
60
 
38
61
  // dispatcher
39
62
  //
40
63
  // Return a new task dispatcher.
41
64
  //
42
- closure dispatcher(data, local reply, restorens, context, expr, roots, proc, args, result) {
65
+ closure dispatcher(data, dynreply, local reply, restorens, context, expr, roots, proc, args, result) {
66
+ if data.reverse {
67
+ data.reverse = false
68
+ contexts[data.contextID].execReceived(data)
69
+ return
70
+ }
71
+ if !dynreply
72
+ dynreply = replier
43
73
  reply = {
44
74
  xmsg: "error"
45
75
  xid: data.xid
@@ -62,6 +92,10 @@ closure TaskDispatcher(replier, local contexts, gohome, coder, util) {
62
92
  namespace: nsactive()
63
93
  activator: gohome
64
94
  }
95
+ if coder
96
+ context.coder = coder
97
+ else
98
+ ContextCoder(data.contextID, data._guid, dynreply)
65
99
  restorens()
66
100
  reply.xmsg = "registered"
67
101
  reply.contextID = data.contextID
@@ -85,11 +119,11 @@ closure TaskDispatcher(replier, local contexts, gohome, coder, util) {
85
119
  intern: true // intern symbols
86
120
  localroots: true // make roots local if needed to intern
87
121
  execute: true // execute auto-running procedures
88
- objectDecoder: coder.decodeObject
122
+ objectDecoder: context.coder.decodeObject
89
123
  })
90
124
  expr = caar(expr)
91
125
  if expr.0 == `\.invoke\-method
92
- expr = coder.invokeMethod(expr.1, expr.2) // object method, args
126
+ expr = context.coder.invokeMethod(expr.1, expr.2) // object method, args
93
127
  else
94
128
  expr = evalactive(expr) // evaluate expression
95
129
  reply.error = undefined
@@ -104,7 +138,7 @@ closure TaskDispatcher(replier, local contexts, gohome, coder, util) {
104
138
  reply.result = util.pkgSave(`expr, roots, {
105
139
  nsignore: true // don't report namespace errors
106
140
  comments: true
107
- objectEncoder: coder.encodeObject}) // include comments
141
+ objectEncoder: context.coder.encodeObject}) // include comments
108
142
  if context.namespace !== nsactive().0
109
143
  { // evaluation changed namespace
110
144
  context.namespace = nsactive().0
@@ -154,7 +188,7 @@ closure TaskDispatcher(replier, local contexts, gohome, coder, util) {
154
188
  } finally {
155
189
  restorens()
156
190
  }
157
- replier(reply)
191
+ dynreply(reply)
158
192
  }
159
193
  };
160
194
 
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * column positioning: // // !
9
9
  *
10
- * Copyright (c) 2017-2024 by Richard C. Zulch
10
+ * Copyright (c) 2017-2025 by Richard C. Zulch
11
11
  *
12
12
  */
13
13
 
@@ -76,21 +76,31 @@ closure dapoTable(database, rootpath, local table, prefix) {
76
76
 
77
77
  // add
78
78
  //
79
- // Add a new record to the table with optional metadata. This will fail if the key already
80
- // exists. Returns the new document record.
81
-
82
- table.add = closure add(key, content, metadata, callback, local id, doc) {
79
+ // Add a new record to the table with options. This will fail if the key already exists. Optional
80
+ // metadata is stored with the record and cannot be changed without calling update. Metadata keys
81
+ // cannot begin with underscore ("_").
82
+ //
83
+ // The returned document, which can be used with update, has the following keys:
84
+ // key - the key name of the record
85
+ // content - the body content stored under in the record
86
+ // _md5 - the MD5 hash of the content
87
+ // _length - the length of the content
88
+ // _id - the PouchDB ID
89
+ // _rev - the PouchDB revision
90
+ // <...> - various metadata keys
91
+
92
+ table.add = closure add(key, content, options, callback, local id, doc) {
83
93
  if !callback
84
- return (syncAdapter(add, key, content, metadata))
94
+ return (syncAdapter(add, key, content, options))
85
95
  id = makeID(key)
86
96
  if !string(id)
87
97
  return (asyncResult(callback, id)) // error from makeID
88
- if metadata && !dictionary(metadata)
98
+ if options.metadata && !dictionary(options.metadata)
89
99
  return (asyncResult(callback, Error("invalid arguments")))
90
100
  if !database.pouch
91
101
  return (asyncResult(callback, Error("database closed")))
92
- if metadata {
93
- doc = new(metadata) // start with metadata
102
+ if options.metadata {
103
+ doc = new(options.metadata) // start with metadata
94
104
  doc._rev = undefined } // _rev not allowed in metadata
95
105
  else
96
106
  doc = { }
@@ -115,12 +125,13 @@ closure dapoTable(database, rootpath, local table, prefix) {
115
125
 
116
126
  // update
117
127
  //
118
- // Update a record in the table using the document previously returned by add or get.
119
- // This will fail if the record has been modified by someone else in the meantime.
128
+ // Update a record in the table using the document previously returned by add or get. Specify
129
+ // options to update those values, otherwise existing/default values are used. This will fail
130
+ // if the record has been modified by someone else in the meantime.
120
131
 
121
- table.update = closure update(doc, callback) {
132
+ table.update = closure update(doc, options, callback) {
122
133
  if !callback
123
- return (syncAdapter(update, doc))
134
+ return (syncAdapter(update, doc, options))
124
135
  if !doc._id || !doc._id.startsWith(table.prefix) || !doc._rev
125
136
  return (asyncResult(callback, Error("invalid argument")))
126
137
  if makeID(doc.key) != doc._id
@@ -130,6 +141,8 @@ closure dapoTable(database, rootpath, local table, prefix) {
130
141
  doc = new(doc)
131
142
  doc._md5 = undefined
132
143
  doc._length = undefined
144
+ if options.metadata
145
+ doc = merge(doc, options.metadata)
133
146
  database.pouch.put(doc, function(error, resp) {
134
147
  if error
135
148
  error = Error("update failed", error)