primate 0.3.1 → 0.4.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 (41) hide show
  1. package/package.json +9 -3
  2. package/source/client/Action.js +12 -15
  3. package/source/client/Client.js +11 -15
  4. package/source/client/Context.js +9 -15
  5. package/source/client/Element.js +8 -3
  6. package/source/client/Session.js +27 -0
  7. package/source/client/document.js +1 -1
  8. package/source/preset/primate.json +2 -6
  9. package/source/server/Action.js +53 -75
  10. package/source/server/App.js +26 -7
  11. package/source/server/Bundler.js +11 -14
  12. package/source/server/Context.js +64 -56
  13. package/source/server/{promises/Eager.js → EagerPromise.js} +3 -1
  14. package/source/server/File.js +1 -1
  15. package/source/server/Projector.js +86 -0
  16. package/source/server/Router.js +7 -9
  17. package/source/server/Session.js +9 -33
  18. package/source/server/attributes.js +2 -0
  19. package/source/server/conf.js +20 -26
  20. package/source/server/{Crypto.js → crypto.js} +0 -0
  21. package/source/server/domain/Domain.js +56 -46
  22. package/source/server/domain/Field.js +16 -13
  23. package/source/server/domain/domains.js +16 -0
  24. package/source/server/errors.js +0 -1
  25. package/source/server/exports.js +9 -8
  26. package/source/server/{utils/extend_object.js → extend_object.js} +0 -0
  27. package/source/server/invariants.js +0 -2
  28. package/source/server/sanitize.js +5 -0
  29. package/source/server/servers/Dynamic.js +19 -13
  30. package/source/server/servers/Static.js +24 -19
  31. package/source/server/store/Store.js +13 -0
  32. package/source/server/types/Domain.js +0 -5
  33. package/source/server/types/Storeable.js +6 -7
  34. package/source/server/view/TreeNode.js +2 -0
  35. package/source/server/view/View.js +5 -0
  36. package/source/client/Base.js +0 -5
  37. package/source/server/Base.js +0 -35
  38. package/source/server/errors/Fallback.js +0 -1
  39. package/source/server/fallback.js +0 -11
  40. package/source/server/promises/Meta.js +0 -42
  41. package/source/server/promises.js +0 -2
package/package.json CHANGED
@@ -1,15 +1,21 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "author": "Primate core team <core@primatejs.com>",
5
5
  "homepage": "https://primatejs.com",
6
+ "bugs": "https://adaptivecloud.dev/primate/primate/issues",
7
+ "repository": "https://adaptivecloud.dev/primate/primate",
6
8
  "description": "Server-client framework",
7
9
  "license": "BSD-3-Clause",
8
10
  "dependencies": {
9
- "ws": "^8.4.0"
11
+ "ws": "^8.4.2"
10
12
  },
11
13
  "scripts": {
12
- "test": "node --experimental-json-modules node_modules/stick stick.json"
14
+ "copy": "rm -rf output && mkdir output && cp source/* output -a",
15
+ "instrument": "nyc instrument source ./output",
16
+ "stick": "node --experimental-json-modules node_modules/stick stick.json",
17
+ "coverage": "yarn instrument && nyc yarn run stick",
18
+ "test": "yarn copy && yarn run stick"
13
19
  },
14
20
  "type": "module",
15
21
  "exports": "./source/server/exports.js",
@@ -1,13 +1,11 @@
1
1
  import Element from "../Element.js";
2
2
  import View from "./View.js";
3
- import Base from "./Base.js";
4
- import {origin_base} from "./document.js";
3
+ import {origin_base, origin} from "./document.js";
5
4
 
6
- export default class Action extends Base {
7
- constructor(name, context) {
8
- super();
5
+ export default class Action {
6
+ constructor(name, session) {
9
7
  this.name = name;
10
- this.context = context;
8
+ this.session = session;
11
9
  this.listeners = [];
12
10
  }
13
11
 
@@ -23,9 +21,9 @@ export default class Action extends Base {
23
21
 
24
22
  create_view(data, root, save_history = true) {
25
23
  if (save_history) {
26
- const pathname = data.pathname === "" ? "/" : data.pathname;
27
- window.location.pathname !== pathname
28
- && history.pushState({}, "", pathname);
24
+ if (data.url !== document.location.href.replace(origin, "")) {
25
+ history.pushState({}, "", data.url);
26
+ }
29
27
  }
30
28
  this.view = new View(data, this, root);
31
29
  }
@@ -73,9 +71,9 @@ export default class Action extends Base {
73
71
  }
74
72
  event.preventDefault();
75
73
  const data = await this.write(target.action, this.get_form_data(target));
76
- return data.pathname === document.location.href.replace(origin_base, "")
74
+ return data.type === "update"
77
75
  ? this.view.update(data.payload)
78
- : this.context.run(data);
76
+ : this.session.run(data);
79
77
  }
80
78
 
81
79
  async onclick(event) {
@@ -96,8 +94,7 @@ export default class Action extends Base {
96
94
  if (event.button === 0 && url.href.startsWith(origin_base)) {
97
95
  event.preventDefault();
98
96
  if (href !== "") {
99
- const data = await this.read(href);
100
- await this.context.run(data);
97
+ await this.session.run(await this.read(href));
101
98
  }
102
99
  }
103
100
  }
@@ -150,10 +147,10 @@ export default class Action extends Base {
150
147
  }
151
148
 
152
149
  read(pathname) {
153
- return this.context.read(pathname);
150
+ return this.session.read(pathname);
154
151
  }
155
152
 
156
153
  write(pathname, payload) {
157
- return this.context.write(pathname, payload);
154
+ return this.session.write(pathname, payload);
158
155
  }
159
156
  }
@@ -1,5 +1,4 @@
1
- import Context from "./Context.js";
2
- import {contexts} from "./exports.js";
1
+ import Session from "./Session.js";
3
2
  import {base, host, origin_base} from "./document.js";
4
3
 
5
4
  const events = ["submit", "click", "change", "input"];
@@ -9,25 +8,25 @@ const location = `wss://${host}${base}`;
9
8
  export default class Client {
10
9
  constructor(conf) {
11
10
  this.conf = conf;
11
+ this.session = new Session(this);
12
12
 
13
13
  window.onpopstate = async event => {
14
14
  const {pathname, search} = event.target.location;
15
- return this.execute(await this.read(pathname + search));
15
+ return this.session.run(await this.read(pathname + search));
16
16
  };
17
17
 
18
18
  for (const key of events) {
19
- window.addEventListener(key, event => this.context.on(key, event));
19
+ window.addEventListener(key, event => this.session.on(key, event));
20
20
  }
21
21
  }
22
22
 
23
23
  open() {
24
24
  return this.connection?.readyState === 1 ? this
25
25
  : new Promise(resolve => {
26
- const connection = new WebSocket(location);
27
- connection.addEventListener("message", ({data}) => data === "open"
26
+ this.connection = new WebSocket(location);
27
+ this.connection.addEventListener("message", ({data}) => data === "open"
28
28
  ? resolve(this)
29
29
  : this.receive(JSON.parse(data)));
30
- this.connection = connection;
31
30
  });
32
31
  }
33
32
 
@@ -36,16 +35,10 @@ export default class Client {
36
35
  back(data);
37
36
  back = undefined;
38
37
  } else {
39
- await this.execute(data);
38
+ await this.session.execute(data);
40
39
  }
41
40
  }
42
41
 
43
- async execute(data) {
44
- const {context} = data;
45
- this.context = new (contexts[context] ?? Context)(context, this);
46
- await this.context.run(data);
47
- }
48
-
49
42
  read(url) {
50
43
  return this.send("read", url);
51
44
  }
@@ -57,9 +50,12 @@ export default class Client {
57
50
  async send(type, url, payload = {}) {
58
51
  await this.open();
59
52
  const {pathname, search} = new URL(url, origin_base);
53
+ const _url = pathname + search;
60
54
  return new Promise(resolve => {
61
55
  back = data => resolve(data);
62
- this.connection.send(JSON.stringify({type, pathname, search, payload}));
56
+ this.connection.send(JSON.stringify({
57
+ type, pathname, search, payload, "url": _url,
58
+ }));
63
59
  });
64
60
  }
65
61
  }
@@ -1,24 +1,26 @@
1
- import Base from "./Base.js";
2
1
  import Action from "./Action.js";
3
2
  import {routes} from "./exports.js";
4
3
 
5
- export default class Context extends Base {
6
- constructor(name, client) {
7
- super();
4
+ export default class Context {
5
+ constructor(name, session) {
8
6
  this.name = name;
9
7
  this.actions = {};
10
8
  this.used_actions = [];
11
- this.client = client;
9
+ this.session = session;
10
+ }
11
+
12
+ get Class() {
13
+ return this.constructor;
12
14
  }
13
15
 
14
16
  async run(data) {
15
17
  const {namespace, action} = data;
16
18
  const route = `${namespace}/${action}`;
17
19
  const module = routes?.[namespace]?.[action] ?? Action;
18
- this.actions[route] = new module(action, this);
20
+ this.actions[route] = new module(action, this.session);
19
21
  const nextaction = this.actions[route];
20
22
  nextaction.enter(data);
21
- await this.Class().before(nextaction);
23
+ await this.Class.before(nextaction);
22
24
  nextaction.before();
23
25
  this.action = nextaction;
24
26
  }
@@ -31,14 +33,6 @@ export default class Context extends Base {
31
33
  return data;
32
34
  }
33
35
 
34
- read(pathname) {
35
- return this.client.read(pathname);
36
- }
37
-
38
- write(pathname, payload) {
39
- return this.client.write(pathname, payload);
40
- }
41
-
42
36
  on(key, event) {
43
37
  this.action[`on${key}`](event);
44
38
  }
@@ -77,8 +77,7 @@ export default class Element {
77
77
  }
78
78
 
79
79
  static value(element, name) {
80
- const attribute = element.attributes.getNamedItem(name);
81
- return attribute === null ? null : attribute.value;
80
+ return element?.attributes.getNamedItem(name)?.value;
82
81
  }
83
82
 
84
83
  static evaluate(attribute, source, data) {
@@ -183,7 +182,8 @@ export default class Element {
183
182
 
184
183
  options(value) {
185
184
  value instanceof Array && value.forEach(option => {
186
- if (this.element.children[option._id] === undefined) {
185
+ const children = [...this.element.children];
186
+ if (!children.find(child => child.value === option._id)) {
187
187
  const newElement = document.createElement("option");
188
188
  newElement.setAttribute("value", option._id);
189
189
  newElement.innerHTML = option.name;
@@ -203,11 +203,13 @@ export default class Element {
203
203
 
204
204
  select_value(value) {
205
205
  this.element.value = value === undefined ? "" : value;
206
+ return this;
206
207
  }
207
208
 
208
209
  input_value(value) {
209
210
  const type = Element.value(this.element, "type");
210
211
  this[`input_${type}_value`]?.(value) ?? this.input_input_value(value);
212
+ return this;
211
213
  }
212
214
 
213
215
  textarea_value(value) {
@@ -218,6 +220,7 @@ export default class Element {
218
220
  if (value === true) {
219
221
  this.element.checked = true;
220
222
  }
223
+ return this;
221
224
  }
222
225
 
223
226
  input_file_value(value) {
@@ -227,12 +230,14 @@ export default class Element {
227
230
  }
228
231
  reader.onload = ({target}) => element.setAttribute("base64", target.result);
229
232
  element.addEventListener("change", file_listener);
233
+ return this;
230
234
  }
231
235
 
232
236
  input_input_value(value) {
233
237
  if (value !== undefined) {
234
238
  this.element.value = value;
235
239
  }
240
+ return this;
236
241
  }
237
242
 
238
243
  default_value(value) {
@@ -0,0 +1,27 @@
1
+ import Context from "./Context.js";
2
+ import {contexts} from "./exports.js";
3
+
4
+ export default class Session {
5
+ constructor(client) {
6
+ this.client = client;
7
+ }
8
+
9
+ async run(data) {
10
+ const {context} = data;
11
+ this.context = new (contexts[context] ?? Context)(context, this);
12
+
13
+ await this.context.run(data);
14
+ }
15
+
16
+ on(key, event) {
17
+ this.context.on(key, event);
18
+ }
19
+
20
+ read(pathname) {
21
+ return this.client.read(pathname);
22
+ }
23
+
24
+ write(pathname, payload) {
25
+ return this.client.write(pathname, payload);
26
+ }
27
+ }
@@ -3,4 +3,4 @@ const {host, href, origin} = document.location;
3
3
  const base = baseURI === href ? "/" : baseURI.replace(origin, "");
4
4
  const origin_base = `${origin}${base}`;
5
5
 
6
- export {base, origin_base, host};
6
+ export {base, origin, origin_base, host};
@@ -2,10 +2,7 @@
2
2
  "base": "/",
3
3
  "debug": false,
4
4
  "defaults": {
5
- "action": "index",
6
- "context": "guest",
7
- "namespace": "default",
8
- "store": "default.js"
5
+ "context": "guest"
9
6
  },
10
7
  "files": {
11
8
  "index": "index.html"
@@ -21,8 +18,7 @@
21
18
  "paths": {
22
19
  "client": "client",
23
20
  "data": {
24
- "domains": "domains",
25
- "stores": "stores"
21
+ "domains": "domains"
26
22
  },
27
23
  "public": "public",
28
24
  "server": "server",
@@ -1,118 +1,96 @@
1
- import Base from "./Base.js";
2
- import {MetaPromise} from "./promises.js";
3
-
4
- const _response = Symbol("#response");
5
-
6
- export default class Action extends Base {
7
- constructor(data, session) {
8
- super();
9
- this._data = data;
1
+ import {resolve} from "path";
2
+ import {default as EagerPromise, eager} from "./EagerPromise.js";
3
+ import View from "./view/View.js";
4
+ import Projector from "./Projector.js";
5
+ import InternalServerError from "./errors/InternalServer.js";
6
+ import {assert} from "./invariants.js";
7
+ import sanitize from "./sanitize.js";
8
+
9
+ export default class Action {
10
+ static action = "index";
11
+ static namespace = "default";
12
+ static layout = "default";
13
+
14
+ constructor(request, session, context) {
15
+ this.request = request;
10
16
  this.session = session;
17
+ this.context = EagerPromise.resolve(context);
11
18
  }
12
19
 
13
- static get layout() {
14
- return "default";
15
- }
16
-
17
- static async load(data, session, context) {
18
- let module;
19
- const server = this.conf.paths.server;
20
- const {namespace, action} = data.path;
20
+ static async new(request, session, context) {
21
+ const {namespace = this.namespace, action = this.action} = request.path;
22
+ assert(!namespace.includes("."), () => {
23
+ throw new InternalServerError("namespace may not include a dot"); });
21
24
  const route = `${namespace}/${action}`;
25
+ const path = resolve(`${context.directory}/${route}.js`);
22
26
  try {
23
- module = (await import(`${server}/${context}/${route}.js`)).default;
27
+ return new (await import(path)).default(request, session, context);
24
28
  } catch (error) {
25
- throw new Error(`route '${route}' missing`);
29
+ throw new Error(`route \`${route}\` missing`);
26
30
  }
27
- return new module(data, session);
31
+ }
32
+
33
+ get Class() {
34
+ return this.constructor;
28
35
  }
29
36
 
30
37
  get layout() {
31
- return this.Class().layout;
38
+ return this.Class.layout;
32
39
  }
33
40
 
34
41
  get path() {
35
- return this._data.path;
42
+ const {path} = this.request;
43
+ const namespace = path.namespace ?? this.Class.namespace;
44
+ const action = path.action ?? this.Class.action;
45
+ const _id = path._id;
46
+ return {action, namespace, _id};
36
47
  }
37
48
 
38
49
  get params() {
39
- return this._data.params;
40
- }
41
-
42
- async _view(path) {
43
- const {namespace, action} = this.path;
44
- const view_path = path !== undefined ? path : `${namespace}/${action}`;
45
- return (await this.context).view(view_path);
46
- }
47
-
48
- static sanitize(payload = {}) {
49
- //assert(typeof payload === "object");
50
- return Object.keys(payload)
51
- .map(key => ({key, "value": payload[key].toString().trim()}))
52
- .map(({key, value}) => ({key, "value":
53
- value === "" ? undefined : value}))
54
- .reduce((o, {key, value}) => {o[key] = value; return o;}, {});
50
+ return this.request.params;
55
51
  }
56
52
 
57
53
  async run(type) {
58
54
  this.before && await this.before();
59
- await this[type](this.Class().sanitize(this._data.payload));
60
- }
61
-
62
- get response() {
63
- return this[_response]();
55
+ await this[type](sanitize(this.request.payload));
64
56
  }
65
57
 
66
- return(data = {}) {
67
- this.respond("return", () => ({"payload": data}));
58
+ return(payload = {}) {
59
+ this.respond("return", () => ({payload}));
68
60
  }
69
61
 
70
62
  view(data = {}, layout = true) {
71
63
  this.respond("view", async () => {
72
- const _view = await this._view();
73
- const payload = await this.prepare(data, _view);
64
+ const {_view, payload} = await this.prepare(data);
74
65
  const view = layout ? _view.file(this.layout) : _view.content;
75
66
  return {view, payload};
76
67
  });
77
68
  }
78
69
 
79
70
  update(data) {
80
- this.respond("update", async () => {
81
- const _view = await this._view();
82
- const payload = await this.prepare(data, _view);
83
- return {payload};
84
- });
71
+ this.respond("update", async () =>
72
+ ({"payload": (await this.prepare(data)).payload})
73
+ );
85
74
  }
86
75
 
87
76
  redirect(location) {
88
77
  this.respond("redirect", () => ({location}));
89
78
  }
90
79
 
91
- get context() {
92
- return this.session.actual_context;
93
- }
94
-
95
- switch_context(name, parameters) {
96
- return this.session.switch_context(name, parameters);
97
- }
98
-
99
- async prepare(data = {}, view) {
100
- (await this.context).Class().prepare(this, data);
101
- return MetaPromise(data, await view.elements(this.layout));
80
+ async prepare(data = {}) {
81
+ const route = `${this.path.namespace}/${this.path.action}`;
82
+ const path = await eager`${this.context.directory}/${route}`;
83
+ const _view = await View.new(path, await this.context.layouts);
84
+ await this.context.Class.prepare(this, data);
85
+ const payload = await new Projector(data, await _view.elements(this.layout)).project(route);
86
+ return {payload, _view};
102
87
  }
103
88
 
104
- respond(type, response) {
105
- this[_response] = async () => {
106
- const data = await response();
107
- data.context = (await this.context).name;
108
- data.namespace = this.path.namespace;
109
- data.action = this.path.action;
110
- if (data.pathname === undefined) {
111
- data.pathname = this._data.pathname.replace(this.conf.base, "")
112
- + (this._data.search ?? "");
113
- }
114
- data.type = type;
115
- return data;
89
+ respond(type, how) {
90
+ this.response = async () => {
91
+ const context = await this.context.name;
92
+ const {url} = this.request;
93
+ return {...await how(), type, context, ...this.path, url};
116
94
  };
117
95
  }
118
96
  }
@@ -1,22 +1,35 @@
1
1
  import {resolve} from "path";
2
- import Base from "./Base.js";
3
2
  import Bundler from "./Bundler.js";
4
3
  import File from "./File.js";
5
4
  import Router from "./Router.js";
6
5
  import DynamicServer from "./servers/Dynamic.js";
7
6
  import StaticServer from "./servers/Static.js";
7
+ import cache from "./cache.js";
8
8
  import log from "./log.js";
9
+ import package_json from "../../package.json" assert {"type": "json" };
9
10
 
10
- export default class App extends Base {
11
- constructor() {
12
- super();
13
- this.router = new Router(this.routes, this.conf);
11
+ export default class App {
12
+ constructor(conf) {
13
+ this.conf = conf;
14
14
  this.Bundler = Bundler;
15
15
  }
16
16
 
17
+ get routes() {
18
+ return cache(this, "routes", async () => {
19
+ try {
20
+ const path = `${this.conf.root}/routes.json`;
21
+ return (await import(path, {"assert": {"type": "json"}})).default;
22
+ } catch (error) {
23
+ // local routes.json not required
24
+ return [];
25
+ }
26
+ });
27
+ }
28
+
17
29
  async run() {
18
- log.reset("Primate").yellow(this.package.version);
30
+ log.reset("Primate").yellow(package_json.version);
19
31
 
32
+ this.router = new Router(await this.routes, this.conf);
20
33
  const {index, hashes} = await new this.Bundler(this.conf).bundle();
21
34
  const router = this.router;
22
35
 
@@ -26,17 +39,23 @@ export default class App extends Base {
26
39
  "key": File.read_sync(resolve(this.conf.http.ssl.key)),
27
40
  "cert": File.read_sync(resolve(this.conf.http.ssl.cert)),
28
41
  },
42
+ "context": this.conf.defaults.context,
29
43
  };
30
44
  this.static_server = new StaticServer(conf);
31
45
  await this.static_server.run();
32
46
 
33
47
  this.dynamic_server = new DynamicServer({router,
34
- "path": this.base,
48
+ "path": this.conf.base,
35
49
  "server": this.static_server.server,
50
+ "context": this.conf.defaults.context,
36
51
  });
37
52
  await this.dynamic_server.run();
38
53
 
39
54
  const {port, host} = this.conf.http;
40
55
  this.static_server.listen(port, host);
41
56
  }
57
+
58
+ stop() {
59
+ this.static_server.close();
60
+ }
42
61
  }
@@ -2,7 +2,7 @@ import {dirname} from "path";
2
2
  import {fileURLToPath} from "url";
3
3
  import Directory from "./Directory.js";
4
4
  import File from "./File.js";
5
- import {algorithm, hash} from "./Crypto.js";
5
+ import {algorithm, hash} from "./crypto.js";
6
6
 
7
7
  const meta_url = fileURLToPath(import.meta.url);
8
8
  const directory = dirname(meta_url);
@@ -25,17 +25,18 @@ const stringify_actions = ({context, domain, actions = []}, i, j) =>
25
25
  actions.reduce((s, action, k) =>
26
26
  s + stringify_action(context, domain, action, i, j, k), "");
27
27
 
28
- const stringify_domain = (domain, i, j) =>
29
- `routes["${domain.context}"]["${domain.domain}"] = {};\n`
28
+ const stringify_domain = (context, domain, i, j) =>
29
+ `routes["${context}"]["${domain.domain}"] = {};\n`
30
30
  + stringify_actions(domain, i, j);
31
31
 
32
- const stringify_domains = (domains, i) =>
33
- domains.reduce((s, domain, j) => s + stringify_domain(domain, i, j), "");
32
+ const stringify_domains = ({context, domains}, i) =>
33
+ domains.reduce((s, domain, j) =>
34
+ s + stringify_domain(context, domain, i, j), "");
34
35
 
35
36
  const stringify_context = (context, i) =>
36
- `import context${i} from "./${context[0].context}/context.js";\n`
37
- + `contexts["${context[0].context}"] = context${i};\n`
38
- + `routes["${context[0].context}"] = {};\n`
37
+ `import context${i} from "./${context.context}/context.js";\n`
38
+ + `contexts["${context.context}"] = context${i};\n`
39
+ + `routes["${context.context}"] = {};\n`
39
40
  + stringify_domains(context, i) + "\n";
40
41
 
41
42
  const s_exports = () =>
@@ -100,11 +101,9 @@ export default class Bundler {
100
101
  }
101
102
 
102
103
  async register_scripts() {
103
- const {paths} = this.conf;
104
-
105
104
  const scripts = await this.collect("client");
106
105
  Promise.all(scripts.map(async script =>
107
- this.register(script, await File.read(paths.public, script))
106
+ this.register(script, await File.read(this.conf.paths.public, script))
108
107
  ));
109
108
  }
110
109
 
@@ -173,8 +172,6 @@ export default class Bundler {
173
172
  .map(action => action.slice(0, -ending)),
174
173
  })))));
175
174
 
176
- return actions.length === 1 && actions[0].length === 0
177
- ? [context_domains]
178
- : actions;
175
+ return context_domains;
179
176
  }
180
177
  }