primate 0.2.0 → 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 (56) hide show
  1. package/README.md +11 -5
  2. package/package.json +9 -3
  3. package/source/client/Action.js +20 -22
  4. package/source/client/Client.js +13 -17
  5. package/source/client/Context.js +10 -16
  6. package/source/client/Element.js +21 -15
  7. package/source/client/Session.js +27 -0
  8. package/source/client/View.js +1 -2
  9. package/source/client/document.js +1 -1
  10. package/source/preset/primate.json +2 -6
  11. package/source/server/Action.js +56 -75
  12. package/source/server/App.js +27 -8
  13. package/source/server/Bundler.js +13 -16
  14. package/source/server/Context.js +63 -56
  15. package/source/server/{promises/Eager.js → EagerPromise.js} +4 -2
  16. package/source/server/File.js +6 -2
  17. package/source/server/Projector.js +86 -0
  18. package/source/server/Router.js +15 -24
  19. package/source/server/Session.js +9 -33
  20. package/source/server/attributes.js +3 -0
  21. package/source/server/conf.js +20 -26
  22. package/source/server/{Crypto.js → crypto.js} +0 -0
  23. package/source/server/domain/Domain.js +60 -48
  24. package/source/server/domain/Field.js +34 -36
  25. package/source/server/domain/Predicate.js +24 -0
  26. package/source/server/domain/domains.js +16 -0
  27. package/source/server/errors/InternalServer.js +1 -1
  28. package/source/server/errors/Predicate.js +1 -1
  29. package/source/server/errors.js +0 -1
  30. package/source/server/exports.js +10 -9
  31. package/source/server/{utils/extend_object.js → extend_object.js} +0 -0
  32. package/source/server/invariants.js +23 -10
  33. package/source/server/sanitize.js +10 -0
  34. package/source/server/servers/Dynamic.js +19 -13
  35. package/source/server/servers/Server.js +1 -1
  36. package/source/server/servers/Static.js +25 -20
  37. package/source/server/servers/content-security-policy.json +0 -1
  38. package/source/server/store/Store.js +13 -0
  39. package/source/server/types/Array.js +2 -2
  40. package/source/server/types/Boolean.js +2 -2
  41. package/source/server/types/Date.js +2 -2
  42. package/source/server/types/Domain.js +1 -6
  43. package/source/server/types/Instance.js +1 -1
  44. package/source/server/types/Number.js +2 -2
  45. package/source/server/types/Object.js +2 -2
  46. package/source/server/types/Primitive.js +1 -1
  47. package/source/server/types/Storeable.js +12 -19
  48. package/source/server/types/String.js +2 -2
  49. package/source/server/view/TreeNode.js +2 -0
  50. package/source/server/view/View.js +6 -1
  51. package/source/client/Base.js +0 -5
  52. package/source/server/Base.js +0 -35
  53. package/source/server/errors/Fallback.js +0 -1
  54. package/source/server/fallback.js +0 -11
  55. package/source/server/promises/Meta.js +0 -42
  56. package/source/server/promises.js +0 -2
package/README.md CHANGED
@@ -18,8 +18,8 @@ yarn add primate
18
18
  * **Minimal** just one dependency ([ws][])
19
19
  * **Simple** only native web technologies (JavaScript, HTML)
20
20
  * **Full-stack** server and client, both in JavaScript
21
- * **Layered** separation of data (domains), logic (actions) and
22
- presentation (views)
21
+ * **Layered** separation of data (domains), logic (actions) and presentation
22
+ (views)
23
23
  * **Correct** data verification using field definitions in domains
24
24
  * **Secure** serving hash-verified scripts on secure HTTP with a strong CSP
25
25
  * **Roles** access control with contexts
@@ -27,15 +27,18 @@ presentation (views)
27
27
  * **Sane defaults** minimally opinionated with good, overrideable defaults
28
28
  for most features
29
29
  * **Data linking** modeling `1:1`, `1:n` and `n:m` relationships on domains via
30
- ad-hoc getters
30
+ cacheable ad-hoc getters
31
31
 
32
32
  ## Getting started
33
33
 
34
- Coming soon.
34
+ See the [getting started][getting-started] guide.
35
35
 
36
36
  ## Resources
37
37
 
38
- Coming soon.
38
+ * [Source code][source-code]
39
+ * [Issues][issues]
40
+
41
+ A full guide is coming soon.
39
42
 
40
43
  ## Versioning
41
44
 
@@ -48,3 +51,6 @@ will move over to semantic versioning.
48
51
  BSD-3-Clause
49
52
 
50
53
  [ws]: https://github.com/websockets/ws
54
+ [getting-started]: https://primatejs.com/getting-started
55
+ [source-code]: https://adaptivecloud.dev/primate/primate
56
+ [issues]: https://adaptivecloud.dev/primate/primate/issues
package/package.json CHANGED
@@ -1,15 +1,21 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.2.0",
3
+ "version": "0.5.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.5.0"
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,19 +1,17 @@
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
 
14
12
  before() {}
15
13
 
16
- async enter(data) {
14
+ enter(data) {
17
15
  if (data.location) {
18
16
  location.href = data.location;
19
17
  } else {
@@ -23,20 +21,20 @@ 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
  }
32
30
 
33
- create_and_return_view(data, root, save_history) {
31
+ create_and_return_view(data, root) {
34
32
  return new View(data, this, root);
35
33
  }
36
34
 
37
35
  get_form_data(target) {
38
36
  const payload = {};
39
- const elements = target.elements;
37
+ const {elements} = target;
40
38
  for (let i = 0; i < elements.length; i++) {
41
39
  const element = elements[i];
42
40
  const type = Element.value(element, "type");
@@ -66,16 +64,16 @@ export default class Action extends Base {
66
64
  }
67
65
 
68
66
  async onsubmit(event) {
69
- const target = event.target;
67
+ const {target} = event;
70
68
  const stop = this.fire("submit", event);
71
69
  if (stop) {
72
70
  return event.preventDefault();
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
  }
@@ -111,13 +108,13 @@ export default class Action extends Base {
111
108
  }
112
109
 
113
110
  oninput() {
114
- const stop = this.fire("input", event);
111
+ this.fire("input", event);
115
112
  }
116
113
 
117
114
  fire(type, event) {
118
115
  const listeners = this.listeners[type];
119
116
  if (listeners !== undefined) {
120
- const target = event.target;
117
+ const {target} = event;
121
118
  for (const listener of listeners) {
122
119
  if (event.target.closest(listener.selector)) {
123
120
  listener.listener(event, target);
@@ -134,7 +131,8 @@ export default class Action extends Base {
134
131
  const index = this.listeners[event]
135
132
  .findIndex(listener => listener.selector === selector);
136
133
  const object = {selector, listener};
137
- if (index === -1) {
134
+ const not_found = -1;
135
+ if (index === not_found) {
138
136
  this.listeners[event].push(object);
139
137
  } else {
140
138
  this.listeners[event].splice(index, 1, object);
@@ -150,10 +148,10 @@ export default class Action extends Base {
150
148
  }
151
149
 
152
150
  read(pathname) {
153
- return this.context.read(pathname);
151
+ return this.session.read(pathname);
154
152
  }
155
153
 
156
154
  write(pathname, payload) {
157
- return this.context.write(pathname, payload);
155
+ return this.session.write(pathname, payload);
158
156
  }
159
157
  }
@@ -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,43 +8,37 @@ 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
 
34
33
  async receive(data) {
35
- if (back !== undefined) {
34
+ if (back === undefined) {
35
+ await this.session.execute(data);
36
+ } else {
36
37
  back(data);
37
38
  back = undefined;
38
- } else {
39
- await this.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
- const module = routes?.[namespace]?.[action] ?? Action;
18
- this.actions[route] = new module(action, this);
19
+ const Module = routes?.[namespace]?.[action] ?? Action;
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
  }
@@ -1,5 +1,5 @@
1
- const file_listener = event => reader.readAsDataURL(event.target.files[0]);
2
1
  const reader = new FileReader();
2
+ const file_listener = event => reader.readAsDataURL(event.target.files[0]);
3
3
 
4
4
  const replace = (attribute, source) => {
5
5
  if (attribute.includes(".")) {
@@ -20,11 +20,11 @@ const fulfill = (attribute, source) => {
20
20
  if (source === undefined) {
21
21
  return undefined;
22
22
  }
23
- let value = attribute.value;
23
+ let {value} = attribute;
24
24
  const matches = [...value.matchAll(data_regex)];
25
25
  if (matches.length > 0) {
26
26
  for (const match of matches) {
27
- const key = match[0];
27
+ const [key] = match;
28
28
  const new_value = replace(match[1], source);
29
29
  value = value.replace(key, new_value);
30
30
  }
@@ -34,7 +34,6 @@ const fulfill = (attribute, source) => {
34
34
  return value;
35
35
  };
36
36
 
37
-
38
37
  const $selector = Symbol("$selector");
39
38
  const $element = Symbol("$element");
40
39
  const $action = Symbol("$action");
@@ -47,13 +46,13 @@ const proxy_handler = {
47
46
  if (property === "on" || property === "insert") {
48
47
  return target[property].bind(target);
49
48
  }
50
- if (target.element !== null) {
51
- return (...args) => target[property] !== undefined
52
- ? target[property](...args)
53
- : receiver;
54
- } else {
49
+ if (target.element === null) {
55
50
  return () => receiver;
56
51
  }
52
+
53
+ return (...args) => target[property] === undefined
54
+ ? receiver
55
+ : target[property](...args);
57
56
  },
58
57
  };
59
58
 
@@ -77,8 +76,7 @@ export default class Element {
77
76
  }
78
77
 
79
78
  static value(element, name) {
80
- const attribute = element.attributes.getNamedItem(name);
81
- return attribute === null ? null : attribute.value;
79
+ return element?.attributes.getNamedItem(name)?.value;
82
80
  }
83
81
 
84
82
  static evaluate(attribute, source, data) {
@@ -143,8 +141,10 @@ export default class Element {
143
141
  return this;
144
142
  }
145
143
 
146
- class(name, set) {
147
- this.element.classList.toggle(name, set !== false);
144
+ class(classes, set) {
145
+ classes.split(" ").forEach(name => {
146
+ this.element.classList.toggle(name, set !== false);
147
+ });
148
148
  return this;
149
149
  }
150
150
 
@@ -181,7 +181,8 @@ export default class Element {
181
181
 
182
182
  options(value) {
183
183
  value instanceof Array && value.forEach(option => {
184
- if (this.element.children[option._id] === undefined) {
184
+ const children = [...this.element.children];
185
+ if (!children.find(child => child.value === option._id)) {
185
186
  const newElement = document.createElement("option");
186
187
  newElement.setAttribute("value", option._id);
187
188
  newElement.innerHTML = option.name;
@@ -201,11 +202,13 @@ export default class Element {
201
202
 
202
203
  select_value(value) {
203
204
  this.element.value = value === undefined ? "" : value;
205
+ return this;
204
206
  }
205
207
 
206
208
  input_value(value) {
207
209
  const type = Element.value(this.element, "type");
208
210
  this[`input_${type}_value`]?.(value) ?? this.input_input_value(value);
211
+ return this;
209
212
  }
210
213
 
211
214
  textarea_value(value) {
@@ -216,21 +219,24 @@ export default class Element {
216
219
  if (value === true) {
217
220
  this.element.checked = true;
218
221
  }
222
+ return this;
219
223
  }
220
224
 
221
225
  input_file_value(value) {
222
- const element = this.element;
226
+ const {element} = this;
223
227
  if (value !== undefined) {
224
228
  element.setAttribute("base64", value);
225
229
  }
226
230
  reader.onload = ({target}) => element.setAttribute("base64", target.result);
227
231
  element.addEventListener("change", file_listener);
232
+ return this;
228
233
  }
229
234
 
230
235
  input_input_value(value) {
231
236
  if (value !== undefined) {
232
237
  this.element.value = value;
233
238
  }
239
+ return this;
234
240
  }
235
241
 
236
242
  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,8 +3,6 @@
3
3
  import Element from "../Element.js";
4
4
  import Node from "./Node.js";
5
5
 
6
- const data_regex = /\${([^}]*)}/g;
7
-
8
6
  export default class View {
9
7
  constructor(data, action, root) {
10
8
  this.sources = {};
@@ -87,4 +85,5 @@ export default class View {
87
85
  write(pathname, payload) {
88
86
  return this.action.write(pathname, payload);
89
87
  }
88
+
90
89
  }
@@ -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,99 @@
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, defined} 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");
24
+ });
21
25
  const route = `${namespace}/${action}`;
26
+ const path = resolve(`${context.directory}/${route}.js`);
22
27
  try {
23
- module = (await import(`${server}/${context}/${route}.js`)).default;
28
+ return new (await import(path)).default(request, session, context);
24
29
  } catch (error) {
25
- throw new Error(`route '${route}' missing`);
30
+ throw new Error(`route \`${route}\` missing`);
26
31
  }
27
- return new module(data, session);
32
+ }
33
+
34
+ get Class() {
35
+ return this.constructor;
28
36
  }
29
37
 
30
38
  get layout() {
31
- return this.Class().layout;
39
+ return this.Class.layout;
32
40
  }
33
41
 
34
42
  get path() {
35
- return this._data.path;
43
+ const {path} = this.request;
44
+ const namespace = path.namespace ?? this.Class.namespace;
45
+ const action = path.action ?? this.Class.action;
46
+ const {_id} = path;
47
+ return {action, namespace, _id};
36
48
  }
37
49
 
38
50
  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;}, {});
51
+ return this.request.params;
55
52
  }
56
53
 
57
54
  async run(type) {
55
+ defined(this[type]);
58
56
  this.before && await this.before();
59
- await this[type](this.Class().sanitize(this._data.payload));
57
+ await this[type](sanitize(this.request.payload));
60
58
  }
61
59
 
62
- get response() {
63
- return this[_response]();
64
- }
65
-
66
- return(data = {}) {
67
- this.respond("return", () => ({"payload": data}));
60
+ return(payload = {}) {
61
+ this.respond("return", () => ({payload}));
68
62
  }
69
63
 
70
64
  view(data = {}, layout = true) {
71
65
  this.respond("view", async () => {
72
- const _view = await this._view();
73
- const payload = await this.prepare(data, _view);
66
+ const {_view, payload} = await this.prepare(data);
74
67
  const view = layout ? _view.file(this.layout) : _view.content;
75
68
  return {view, payload};
76
69
  });
77
70
  }
78
71
 
79
72
  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
- });
73
+ this.respond("update", async () =>
74
+ ({"payload": (await this.prepare(data)).payload})
75
+ );
85
76
  }
86
77
 
87
78
  redirect(location) {
88
79
  this.respond("redirect", () => ({location}));
89
80
  }
90
81
 
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));
82
+ async prepare(data = {}) {
83
+ const route = `${this.path.namespace}/${this.path.action}`;
84
+ const path = await eager`${this.context.directory}/${route}`;
85
+ const _view = await View.new(path, await this.context.layouts);
86
+ await this.context.Class.prepare(this, data);
87
+ const payload = await new Projector(data,
88
+ await _view.elements(this.layout)).project(route);
89
+ return {payload, _view};
102
90
  }
103
91
 
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;
92
+ respond(type, how) {
93
+ this.response = async () => {
94
+ const context = await this.context.name;
95
+ const {url} = this.request;
96
+ return {...await how(), type, context, ...this.path, url};
116
97
  };
117
98
  }
118
99
  }