Compare commits

...

21 Commits

Author SHA1 Message Date
cha0s
2ab82d1d3e chore: coverage and tidy 2024-06-30 14:29:10 -05:00
cha0s
d00bcf23f3 chore: gardening 2024-06-30 13:47:00 -05:00
cha0s
d7a629db7a sandbox: more fixes and tests 2024-06-30 13:07:26 -05:00
cha0s
00862f96bd fix: null alternate 2024-06-30 11:27:42 -05:00
cha0s
398dd66594 refactor!: sandbox 2024-06-30 11:18:26 -05:00
cha0s
31722ab4a0 fun: lv 3 tools 2024-06-29 12:36:01 -05:00
cha0s
bb7435e18e perf: sandbox reset 2024-06-29 12:35:54 -05:00
cha0s
89c32a2299 fix: resilience 2024-06-29 11:25:22 -05:00
cha0s
5db2e4905c fun: shit shack 2024-06-29 10:30:37 -05:00
cha0s
13f49813a8 chore: ignore 2024-06-29 09:53:14 -05:00
cha0s
aa586a98e5 dev: bit nicer HMR 2024-06-29 09:52:35 -05:00
cha0s
bcbef693cc feat: removeData 2024-06-29 09:52:23 -05:00
cha0s
b3087ac4d3 chore: tidy 2024-06-29 09:51:10 -05:00
cha0s
6c7221a7f7 fix: async 2024-06-29 09:50:18 -05:00
cha0s
1a3a8f28d0 chore: tidy 2024-06-29 09:12:54 -05:00
cha0s
90d8ee3a35 refactor: controlled instance 2024-06-29 07:27:40 -05:00
cha0s
a53be624d3 feat: insecure HTTP 2024-06-29 06:28:27 -05:00
cha0s
221666783d fix: storybook 2024-06-29 06:28:03 -05:00
cha0s
853f9f1fb4 fix: remote host 2024-06-28 18:30:07 -05:00
cha0s
2830764f9f fix: SSR 2024-06-28 18:28:25 -05:00
cha0s
ee68d0414e fix: secure 2024-06-28 18:04:23 -05:00
37 changed files with 957 additions and 431 deletions

1
.gitignore vendored
View File

@ -4,4 +4,5 @@ node_modules
/.cache /.cache
/build /build
/coverage /coverage
/dev
.env .env

View File

@ -41,6 +41,7 @@ export default function evaluate(node, {scope} = {}) {
return evaluators.unary(node, {evaluate, scope}); return evaluators.unary(node, {evaluate, scope});
case 'UpdateExpression': case 'UpdateExpression':
return evaluators.update(node, {evaluate, scope}); return evaluators.update(node, {evaluate, scope});
/* v8 ignore next 2 */
default: default:
throw new EvalError(`astride: Can't evaluate node of type ${node.type}`) throw new EvalError(`astride: Can't evaluate node of type ${node.type}`)
} }

View File

@ -1,12 +1,10 @@
import {isSpreadElement} from '@/astride/types.js';
export default function(node, {evaluate, scope}) { export default function(node, {evaluate, scope}) {
const elements = []; const elements = [];
const asyncSpread = Object.create(null); const asyncSpread = Object.create(null);
let isAsync = false; let isAsync = false;
for (const index in node.elements) { for (const index in node.elements) {
const element = node.elements[index]; const element = node.elements[index];
if (isSpreadElement(element)) { if ('SpreadElement' === element.type) {
const {async, value} = evaluate(element.argument, {scope}); const {async, value} = evaluate(element.argument, {scope});
isAsync = isAsync || async; isAsync = isAsync || async;
if (async) { if (async) {

View File

@ -1,11 +1,7 @@
import {
isMemberExpression,
} from '@/astride/types.js';
export default function(node, {evaluate, scope}) { export default function(node, {evaluate, scope}) {
const {operator, left} = node; const {operator, left} = node;
const right = evaluate(node.right, {scope}); const right = evaluate(node.right, {scope});
if (!isMemberExpression(left)) { if (!('MemberExpression' === left.type)) {
const assign = (value) => { const assign = (value) => {
switch (operator) { switch (operator) {
case '=' : return scope.set(left.name, value); case '=' : return scope.set(left.name, value);

View File

@ -41,5 +41,8 @@ export default function(node, {evaluate, scope}) {
.then(([left, right]) => binary(left, right)), .then(([left, right]) => binary(left, right)),
}; };
} }
return {value: binary(left.value, right.value)}; return {
async: false,
value: binary(left.value, right.value),
};
} }

View File

@ -1,7 +1,4 @@
import fastCall from '@/util/fast-call.js'; import fastCall from '@/util/fast-call.js';
import {
isMemberExpression,
} from '@/astride/types.js';
export default function(node, {evaluate, scope}) { export default function(node, {evaluate, scope}) {
let asyncArgs = false; let asyncArgs = false;
@ -24,7 +21,7 @@ export default function(node, {evaluate, scope}) {
} }
return fastCall(fn, holder, args); return fastCall(fn, holder, args);
}; };
if (!isMemberExpression(callee)) { if (!('MemberExpression' === callee.type)) {
const {async, value} = evaluate(callee, {scope}); const {async, value} = evaluate(callee, {scope});
if (asyncArgs || async) { if (asyncArgs || async) {
return { return {
@ -49,5 +46,8 @@ export default function(node, {evaluate, scope}) {
.then(([O, P, args]) => invoke(callee.optional ? O?.[P] : O[P], O, args)), .then(([O, P, args]) => invoke(callee.optional ? O?.[P] : O[P], O, args)),
}; };
} }
return {value: invoke(callee.optional ? O.value?.[P.value] : O.value[P.value], O.value, args)}; return {
async: false,
value: invoke(callee.optional ? O.value?.[P.value] : O.value[P.value], O.value, args),
};
} }

View File

@ -48,10 +48,8 @@ scopeTest('evaluates optional calls', async ({scope}) => {
scope.set('O', {}); scope.set('O', {});
expect(evaluate(await expression('g?.(1, 2, 3)'), {scope}).value) expect(evaluate(await expression('g?.(1, 2, 3)'), {scope}).value)
.to.equal(undefined); .to.equal(undefined);
// expect(evaluate(await expression('O?.g(1, 2, 3)'), {scope}).value) expect(evaluate(await expression('O?.g?.(1, 2, 3)'), {scope}).value)
// .to.equal(undefined); .to.equal(undefined);
// expect(evaluate(await expression('O?.g?.(1, 2, 3)'), {scope}).value)
// .to.equal(undefined);
}); });
scopeTest('evaluates async calls', async ({scope}) => { scopeTest('evaluates async calls', async ({scope}) => {

View File

@ -1,3 +1,3 @@
export default function(node, {scope}) { export default function(node, {scope}) {
return {value: scope.get(node.name)}; return {async: false, value: scope.get(node.name)};
} }

View File

@ -1,3 +1,3 @@
export default function({value}) { export default function({value}) {
return {value}; return {async: false, value};
} }

View File

@ -1,6 +1,6 @@
export default function(node, {evaluate, scope}) { export default function(node, {evaluate, scope}) {
const {computed, object, property, wrapper} = node; const {computed, object, optional, property} = node;
const member = (O, P) => (wrapper?.optional ? O?.[P] : O[P]); const member = (O, P) => (optional ? O?.[P] : O[P]);
const O = evaluate(object, {scope}); const O = evaluate(object, {scope});
const P = computed const P = computed
? evaluate(property, {scope}) ? evaluate(property, {scope})
@ -12,5 +12,5 @@ export default function(node, {evaluate, scope}) {
value: Promise.all([O.value, P.value]).then(([O, P]) => member(O, P)), value: Promise.all([O.value, P.value]).then(([O, P]) => member(O, P)),
}; };
} }
return {value: member(O.value, P.value)}; return {async: false, value: member(O.value, P.value)};
} }

View File

@ -1,25 +1,18 @@
import {
isIdentifier,
isLiteral,
isProperty,
isSpreadElement,
} from '@/astride/types.js';
export default function(node, {evaluate, scope}) { export default function(node, {evaluate, scope}) {
const {properties} = node; const {properties} = node;
let isAsync = false; let isAsync = false;
const entries = []; const entries = [];
for (let i = 0; i < properties.length; i++) { for (let i = 0; i < properties.length; i++) {
if (isProperty(properties[i])) { if ('Property' === properties[i].type) {
const {computed, key, value} = properties[i]; const {computed, key, value} = properties[i];
let k; let k;
if (computed) { if (computed) {
k = evaluate(key, {scope}); k = evaluate(key, {scope});
} }
else if (isIdentifier(key)) { else if ('Identifier' === key.type) {
k = {value: key.name}; k = {value: key.name};
} }
else if (isLiteral(key)) { else if ('Literal' === key.type) {
k = {value: key.value}; k = {value: key.value};
} }
/* v8 ignore next 3 */ /* v8 ignore next 3 */
@ -35,7 +28,7 @@ export default function(node, {evaluate, scope}) {
entries.push([k.value, v.value]); entries.push([k.value, v.value]);
} }
} }
if (isSpreadElement(properties[i])) { if ('SpreadElement' === properties[i].type) {
const {argument} = properties[i]; const {argument} = properties[i];
const spreading = evaluate(argument, {scope}); const spreading = evaluate(argument, {scope});
isAsync ||= spreading.async; isAsync ||= spreading.async;

View File

@ -20,5 +20,5 @@ export default function(node, {evaluate, scope}) {
value: Promise.resolve(arg.value).then(unary), value: Promise.resolve(arg.value).then(unary),
}; };
} }
return {value: unary(arg.value)}; return {async: false, value: unary(arg.value)};
} }

View File

@ -19,5 +19,5 @@ export default function(node, {evaluate, scope}) {
/* v8 ignore next */ /* v8 ignore next */
throw new Error(`operator not implemented: ${operator}`); throw new Error(`operator not implemented: ${operator}`);
}; };
return {value: update(value)}; return {async: false, value: update(value)};
} }

View File

@ -1,24 +1,16 @@
import evaluate from '@/astride/evaluate.js'; import evaluate from '@/astride/evaluate.js';
import Scope from '@/astride/scope.js'; import Scope from '@/astride/scope.js';
import traverse, {TRAVERSAL_PATH} from '@/astride/traverse.js'; import traverse from '@/astride/traverse.js';
import {
isArrayPattern, const YIELD_NONE = 0;
isBlockStatement, const YIELD_PROMISE = 1;
isDoWhileStatement, const YIELD_LOOP_UPDATE = 2;
isExpressionStatement, const YIELD_RETURN = 3;
isForStatement,
isIdentifier,
isIfStatement,
isObjectPattern,
isReturnStatement,
isVariableDeclarator,
isWhileStatement,
} from '@/astride/types.js';
export default class Sandbox { export default class Sandbox {
ast; ast;
generator; $$execution;
scopes; scopes;
constructor(ast, context = {}) { constructor(ast, context = {}) {
@ -30,13 +22,13 @@ export default class Sandbox {
compile() { compile() {
let scope = new Scope(); let scope = new Scope();
scope.context = this.$$context; scope.context = this.$$context;
this.scopes = new WeakMap([[this.ast, scope]]); this.scopes = new Map([[this.ast, scope]]);
traverse( traverse(
this.ast, this.ast,
(node, verb) => { (node, verb) => {
if ( if (
isBlockStatement(node) 'BlockStatement' === node.type
|| isForStatement(node) || 'ForStatement' === node.type
) { ) {
switch (verb) { switch (verb) {
case 'enter': { case 'enter': {
@ -68,7 +60,7 @@ export default class Sandbox {
if (null === element) { if (null === element) {
continue; continue;
} }
if (isIdentifier(element)) { if ('Identifier' === element.type) {
scope.allocate(element.name, init[i]); scope.allocate(element.name, init[i]);
} }
/* v8 ignore next 3 */ /* v8 ignore next 3 */
@ -76,7 +68,7 @@ export default class Sandbox {
throw new Error(`destructureArray(): Can't array destructure type ${element.type}`); throw new Error(`destructureArray(): Can't array destructure type ${element.type}`);
} }
} }
return undefined; return init;
} }
destructureObject(id, init) { destructureObject(id, init) {
@ -84,8 +76,8 @@ export default class Sandbox {
const {properties} = id; const {properties} = id;
for (let i = 0; i < properties.length; ++i) { for (let i = 0; i < properties.length; ++i) {
const property = properties[i]; const property = properties[i];
if (isObjectPattern(property.value)) { if ('ObjectPattern' === property.value.type) {
this.destructureObject(property.value, init[property.key.name], scope); this.destructureObject(property.value, init[property.key.name]);
} }
else { else {
scope.allocate( scope.allocate(
@ -94,6 +86,7 @@ export default class Sandbox {
); );
} }
} }
return init;
} }
evaluate(node) { evaluate(node) {
@ -103,161 +96,408 @@ export default class Sandbox {
); );
} }
*execute(node, parent) { evaluateToResult(node) {
let keys = TRAVERSAL_PATH[node.type]; const {async, value} = this.evaluate(node);
if (isVariableDeclarator(node)) { return {
const {id} = node; value,
const scope = this.scopes.get(node); yield: async ? YIELD_PROMISE : YIELD_NONE,
if (null === node.init) { };
scope.allocate(id.name, undefined); }
}
else { executeSync(node, depth) {
const init = this.evaluate(node.init); let result;
if (isIdentifier(id)) { const isReplaying = depth < this.$$execution.stack.length;
if (init.async) { const isReplayingThisLevel = depth === this.$$execution.stack.length - 1;
yield { if (!isReplaying) {
async: true, this.$$execution.stack.push(node);
value: Promise.resolve(init.value).then((value) => { }
scope.allocate(id.name, value); // Substitute executing the node for its deferred result.
}), else if (isReplayingThisLevel) {
}; if (this.$$execution.stack[depth] === node) {
} result = this.$$execution.deferred.get(node);
else { this.$$execution.deferred.delete(node);
scope.allocate(id.name, init.value); this.$$execution.stack.pop();
} return result;
}
else if (isArrayPattern(id)) {
const promiseOrVoid = init.async
? Promise.resolve(init.value).then((init) => this.destructureArray(id, init))
: this.destructureArray(id, init.value);
if (promiseOrVoid) {
yield {
async: true,
value: promiseOrVoid,
};
}
}
else if (isObjectPattern(id)) {
const promiseOrVoid = init.async
? Promise.resolve(init.value).then((init) => this.destructureObject(id, init))
: this.destructureObject(id, init.value);
if (promiseOrVoid) {
yield {
async: true,
value: promiseOrVoid,
};
}
}
} }
} }
// Blocks... switch (node.type) {
if (isIfStatement(node)) { case 'ArrayExpression':
const {async, value} = this.evaluate(node.test); case 'AssignmentExpression':
const branch = (value) => { case 'BinaryExpression':
keys = [value ? 'consequent' : 'alternate']; case 'CallExpression':
}; case 'ChainExpression':
if (async) { case 'ObjectExpression':
yield { case 'Identifier':
async: true, case 'LogicalExpression':
value: Promise.resolve(value).then(branch), case 'MemberExpression':
}; case 'UnaryExpression':
} case 'UpdateExpression': {
else { result = this.evaluateToResult(node);
branch(value); if (result.yield) {
}
}
// Loops...
let loop = false;
if (isForStatement(node)) {
const {value} = this.execute(node.init, node).next();
if (value?.async) {
yield value;
}
}
do {
if (
isForStatement(node)
|| isWhileStatement(node)
) {
const {async, value} = this.evaluate(node.test);
if (async) {
yield {
async: true,
value: Promise.resolve(value).then((value) => {
keys = value ? ['body'] : [];
}),
};
}
else {
keys = value ? ['body'] : [];
}
loop = keys.length > 0;
}
// Recur...
let children = [];
if (keys instanceof Function) {
children = keys(node);
}
else {
for (const key of keys) {
children.push(...(Array.isArray(node[key]) ? node[key] : [node[key]]));
}
}
for (const child of children) {
if (!child) {
continue;
}
const result = yield* this.execute(child, node);
if (result) {
return result; return result;
} }
break;
} }
// Loops... case 'AwaitExpression': {
if (isForStatement(node)) { const coerce = !this.$$execution.deferred.has(node.argument);
const result = this.execute(node.update, node).next(); result = this.executeSync(node.argument, depth + 1);
if (result.value?.async) { if (coerce) {
yield result.value; result = {
} value: result.value,
} yield: YIELD_PROMISE,
if (isDoWhileStatement(node)) {
const test = this.evaluate(node.test);
if (test.async) {
yield {
async: true,
value: Promise.resolve(test.value).then((value) => {
loop = value;
}),
}; };
} }
else { if (result.yield) {
loop = test.value; return result;
} }
/* v8 ignore next 2 */
break;
} }
} while (loop); case 'BlockStatement': {
// Top-level return. result = {
if (isReturnStatement(node)) { value: undefined,
return !node.argument ? {value: undefined} : this.evaluate(node.argument); yield: YIELD_NONE,
} };
if (isExpressionStatement(node)) { let skipping = isReplaying;
yield this.evaluate(node.expression); for (const child of node.body) {
} if (skipping && child === this.$$execution.stack[depth + 1]) {
// yield ForStatement afterthought. skipping = false;
if (isForStatement(parent) && node === parent.update) { }
yield this.evaluate(node); /* v8 ignore next 3 */
/* v8 ignore next */ if (skipping) {
continue;
}
result = this.executeSync(child, depth + 1);
if (result.yield) {
return result;
}
}
break;
}
case 'ConditionalExpression': {
const shouldVisitChild = (child) => (
isReplaying
? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
: true
);
if (shouldVisitChild(node.test)) {
const test = this.executeSync(node.test, depth + 1);
if (test.yield) {
return test;
}
this.$$execution.deferred.set(node.test, test);
}
const test = this.$$execution.deferred.get(node.test);
if (test.value) {
result = this.executeSync(node.consequent, depth + 1);
}
else {
result = this.executeSync(node.alternate, depth + 1);
}
if (result.yield) {
return result;
}
break;
}
case 'DoWhileStatement': {
const shouldVisitChild = (child) => (
isReplaying
? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
: true
);
if (this.$$execution.stack[depth + 1] === node) {
this.$$execution.stack.pop();
}
result = this.$$execution.deferred.get(node.body);
if (shouldVisitChild(node.body)) {
const body = this.executeSync(node.body, depth + 1);
if (body.yield) {
return body;
}
this.$$execution.deferred.set(node.body, body);
}
if (shouldVisitChild(node.test)) {
const test = this.executeSync(node.test, depth + 1);
if (test.yield) {
return test;
}
if (!test.value) {
this.$$execution.deferred.delete(node.body);
break;
}
}
// Yield
this.$$execution.stack.push(node);
return {
value: undefined,
yield: YIELD_LOOP_UPDATE,
};
}
case 'ExpressionStatement': {
result = this.executeSync(node.expression, depth + 1);
if (result.yield) {
return result;
}
break;
}
case 'ForStatement': {
const shouldVisitChild = (child) => (
isReplaying
? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
: true
);
if (shouldVisitChild(node.init)) {
const init = this.executeSync(node.init, depth + 1);
if (init.yield) {
return init;
}
}
if (this.$$execution.stack[depth + 1] === node) {
this.$$execution.stack.pop();
}
result = this.$$execution.deferred.get(node.body) || {
value: undefined,
yield: YIELD_NONE,
};
if (shouldVisitChild(node.test)) {
const test = this.executeSync(node.test, depth + 1);
if (test.yield) {
return test;
}
if (!test.value) {
this.$$execution.deferred.delete(node.body);
break;
}
}
if (shouldVisitChild(node.body)) {
const body = this.executeSync(node.body, depth + 1);
if (body.yield) {
return body;
}
this.$$execution.deferred.set(node.body, body);
}
if (shouldVisitChild(node.update)) {
const update = this.executeSync(node.update, depth + 1);
if (update.yield) {
return update;
}
this.$$execution.deferred.set(node.update, update);
}
// Yield
this.$$execution.stack.push(node);
const update = this.$$execution.deferred.get(node.update);
this.$$execution.deferred.delete(node.update);
return {
value: update.value,
yield: YIELD_LOOP_UPDATE,
};
}
case 'IfStatement': {
const shouldVisitChild = (child) => (
isReplaying
? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
: true
);
result = {
value: undefined,
yield: YIELD_NONE,
};
if (shouldVisitChild(node.test)) {
const test = this.executeSync(node.test, depth + 1);
if (test.yield) {
return test;
}
this.$$execution.deferred.set(node.test, test);
}
const test = this.$$execution.deferred.get(node.test);
if (test.value) {
result = this.executeSync(node.consequent, depth + 1);
}
else if (node.alternate) {
result = this.executeSync(node.alternate, depth + 1);
}
if (result.yield) {
return result;
}
this.$$execution.deferred.delete(node.test);
break;
}
case 'Literal': {
result = {value: node.value, yield: YIELD_NONE};
break;
}
case 'Program': {
let skipping = isReplaying;
for (const child of node.body) {
if (skipping && child === this.$$execution.stack[depth + 1]) {
skipping = false;
}
if (skipping) {
continue;
}
result = this.executeSync(child, depth + 1);
if (result.yield) {
return result;
}
}
result = {value: result.value, yield: YIELD_RETURN};
break;
}
case 'ReturnStatement': {
if (!node.argument) {
return {
value: undefined,
yield: YIELD_RETURN,
};
}
const argument = this.executeSync(node.argument, depth + 1);
if (argument.yield) {
return argument;
}
result = {
value: argument.value,
yield: YIELD_RETURN,
};
break;
}
case 'VariableDeclaration': {
let skipping = isReplaying;
for (const child of node.declarations) {
if (skipping && child === this.$$execution.stack[depth + 1]) {
skipping = false;
}
/* v8 ignore next 3 */
if (skipping) {
continue;
}
const result = this.executeSync(child, depth + 1);
if (result.yield) {
return result;
}
}
result = {value: undefined, yield: YIELD_NONE};
break;
}
case 'VariableDeclarator': {
const {id} = node;
const scope = this.scopes.get(node);
if (null === node.init) {
scope.allocate(id.name, undefined);
result = {value: undefined, yield: YIELD_NONE};
}
else {
const init = this.executeSync(node.init, depth + 1);
if ('Identifier' === id.type) {
if (init.yield) {
return {
value: Promise.resolve(init.value)
.then((value) => scope.allocate(id.name, value)),
yield: init.yield,
};
}
else {
result = {
value: scope.allocate(id.name, init.value),
yield: YIELD_NONE,
};
}
}
else if ('ArrayPattern' === id.type) {
if (init.yield) {
return {
value: Promise.resolve(init.value)
.then((value) => this.destructureArray(id, value)),
yield: init.yield,
};
}
else {
result = {
value: this.destructureArray(id, init.value),
yield: YIELD_NONE,
};
}
}
else if ('ObjectPattern' === id.type) {
if (init.yield) {
return {
value: Promise.resolve(init.value)
.then((value) => this.destructureObject(id, value)),
yield: init.yield,
};
}
else {
result = {
value: this.destructureObject(id, init.value),
yield: YIELD_NONE,
};
}
}
}
break;
}
case 'WhileStatement': {
const shouldVisitChild = (child) => (
isReplaying
? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
: true
);
if (this.$$execution.stack[depth + 1] === node) {
this.$$execution.stack.pop();
}
result = this.$$execution.deferred.get(node.body) || {
value: undefined,
yield: YIELD_NONE,
};
if (shouldVisitChild(node.test)) {
const test = this.executeSync(node.test, depth + 1);
if (test.yield) {
return test;
}
if (!test.value) {
this.$$execution.deferred.delete(node.body);
break;
}
}
if (shouldVisitChild(node.body)) {
const body = this.executeSync(node.body, depth + 1);
if (body.yield) {
return body;
}
this.$$execution.deferred.set(node.body, body);
}
// Yield
this.$$execution.stack.push(node);
return {
value: undefined,
yield: YIELD_LOOP_UPDATE,
};
}
/* v8 ignore next 7 */
default:
console.log(
node.type,
Object.keys(node)
.filter((key) => !['start', 'end'].includes(key)),
);
throw new Error('not implemented');
} }
this.$$execution.stack.pop();
return result;
} }
reset() { reset() {
this.generator = undefined; this.$$execution = undefined;
this.compile(); for (const scope of this.scopes.values()) {
scope.context = {};
}
this.scopes.get(this.ast).context = this.$$context;
} }
run(ops = 1000) { run(ops = 1000) {
let result; let result;
for (let i = 0; i < ops; ++i) { for (let i = 0; i < ops; ++i) {
result = this.step(); result = this.step();
if (result.done || result.value?.async) { if (result.done || result.async) {
break; break;
} }
} }
@ -265,14 +505,40 @@ export default class Sandbox {
} }
step() { step() {
if (!this.generator) { if (!this.$$execution) {
this.generator = this.execute(this.ast); this.$$execution = {
deferred: new Map(),
stack: [],
};
} }
const result = this.generator.next(); let result = this.executeSync(this.ast, 0);
if (result.done) { const stepResult = {async: false, done: false, value: undefined};
switch (result.yield) {
case YIELD_PROMISE: {
stepResult.async = true;
stepResult.value = Promise.resolve(result.value)
.then((value) => {
const top = this.$$execution.stack[this.$$execution.stack.length - 1];
this.$$execution.deferred.set(top, {
value,
yield: YIELD_NONE,
});
});
break;
}
case YIELD_LOOP_UPDATE: {
stepResult.value = result.value;
break;
}
case YIELD_RETURN: {
stepResult.done = true;
stepResult.value = result.value;
}
}
if (stepResult.done) {
this.reset(); this.reset();
} }
return result; return stepResult;
} }
} }

View File

@ -0,0 +1,159 @@
import {parse as acornParse} from 'acorn';
import {expect, test} from 'vitest';
import Sandbox from '@/astride/sandbox.js';
function parse(code, options = {}) {
return acornParse(code, {
ecmaVersion: 'latest',
sourceType: 'module',
...options,
})
}
const testCases = [
// [
// `
// const {one, two: {three, four: {five, six: {seven}}}} = value;
// `,
// {
// value: {
// one: 1,
// two: {
// three: 3,
// four: {
// five: 5,
// six: {
// seven: 7,
// },
// },
// },
// },
// },
// {
// one: 1,
// three: 3,
// five: 5,
// seven: 7
// },
// ],
[
`
const x = 123;
{
const x = 234;
}
`,
{},
{
x: 123,
},
],
[
`
let x = [];
for (let i = 0; i < 100; ++i) {
x[i] = i;
}
`,
{},
{
x: Array(100).fill(0).map((n, i) => i),
},
],
[
`
let x = 0, y;
for (let i = 0; i < 4; ++i) {
x += 1;
}
if (x % 2) {
y = false
}
else {
y = true
}
`,
{},
{
y: true,
},
],
[
`
let x = 0;
while (x < 100) {
x += 1;
}
`,
{},
{
x: 100,
},
],
[
`
let x = 0;
do {
x += 1;
} while (x < 100);
`,
{},
{
x: 100,
},
],
[
`
let x = 0;
do {
x += 1;
} while (x < 100);
`,
{},
{
x: 100,
},
],
]
test('performs well', async () => {
let sandbox;
for (const testCase of testCases) {
sandbox = new Sandbox(await parse(testCase[0]));
for (const key in testCase[1]) {
sandbox.context[key] = testCase[1][key];
}
sandbox.run();
expect(sandbox.context)
.to.deep.include(testCase[2]);
const N = 1000;
let last;
for (let i = 0; i < 100000 / N; ++i) {
sandbox.run();
}
last = performance.now();
for (let i = 0; i < N; ++i) {
sandbox.run();
}
const astrideSyncTime = (performance.now() - last) / N;
const native = new Function(
Object.keys(testCase[1]).map((arg) => `{${arg}}`).join(', '),
testCase[0],
);
for (let i = 0; i < 100000 / N; ++i) {
native(testCase[1]);
}
last = performance.now();
for (let i = 0; i < N; ++i) {
native(testCase[1]);
}
const nativeTime = (performance.now() - last) / N;
// console.log(
// testCase[0],
// `${Math.round(astrideSyncTime / nativeTime)}x slower`,
// );
expect(astrideSyncTime)
.to.be.lessThan(nativeTime * 500);
}
});

View File

@ -11,6 +11,18 @@ function parse(code, options = {}) {
}) })
} }
async function finish(sandbox) {
let result;
let i = 0;
do {
result = sandbox.step();
if (result.async) {
await result.value;
}
} while (!result.done && ++i < 1000);
return result;
}
test('declares variables', async () => { test('declares variables', async () => {
const sandbox = new Sandbox( const sandbox = new Sandbox(
await parse(` await parse(`
@ -22,13 +34,7 @@ test('declares variables', async () => {
const asyncObject = await {11: '12'}; const asyncObject = await {11: '12'};
`), `),
); );
let result; await finish(sandbox);
do {
result = sandbox.step();
if (result.value?.async) {
await result.value.async;
}
} while (!result.done);
expect(sandbox.context) expect(sandbox.context)
.to.deep.equal({ .to.deep.equal({
scalar: 1, scalar: 1,
@ -53,17 +59,24 @@ test('scopes variables', async () => {
result.push(scalar); result.push(scalar);
`), `),
); );
let result; await finish(sandbox);
do {
result = sandbox.step();
if (result.value?.async) {
await result.value.async;
}
} while (!result.done);
expect(sandbox.context.result) expect(sandbox.context.result)
.to.deep.equal([1, 2, 1]); .to.deep.equal([1, 2, 1]);
}); });
test('returns last', async () => {
const sandbox = new Sandbox(
await parse(`
foo = 32;
{
bar = 64;
}
`),
);
expect(await finish(sandbox))
.to.deep.include({done: true, value: 64});
});
test('destructures variables', async () => { test('destructures variables', async () => {
const sandbox = new Sandbox( const sandbox = new Sandbox(
await parse(` await parse(`
@ -73,13 +86,7 @@ test('destructures variables', async () => {
const {t, u: {uu}} = {t: 9, u: {uu: await 10}}; const {t, u: {uu}} = {t: 9, u: {uu: await 10}};
`), `),
); );
let result; await finish(sandbox);
do {
result = sandbox.step();
if (result.value?.async) {
await result.value.value;
}
} while (!result.done);
expect(sandbox.context) expect(sandbox.context)
.to.deep.equal({ .to.deep.equal({
a: 1, a: 1,
@ -98,19 +105,17 @@ test('runs arbitrary number of ops', async () => {
const sandbox = new Sandbox( const sandbox = new Sandbox(
await parse(` await parse(`
const foo = []; const foo = [];
for (let i = 0; i < 1500; ++i) { for (let i = 0; i < 150; ++i) {
foo.push(i); foo.push(i);
} }
`), `),
); );
sandbox.run(1000); sandbox.run(100);
expect(sandbox.context.foo.length) expect(sandbox.context.foo.length)
.to.equal(1000); .to.equal(100);
sandbox.run(1000); sandbox.run(100);
expect(sandbox.context.foo.length) expect(sandbox.context.foo.length)
.to.equal(1500); .to.equal(150);
expect(true)
.to.be.true;
}); });
test('evaluates conditional branches', async () => { test('evaluates conditional branches', async () => {
@ -129,15 +134,12 @@ test('evaluates conditional branches', async () => {
else { else {
bar = 2; bar = 2;
} }
if (await false) {
bar = 1;
}
`), `),
); );
let result; await finish(sandbox);
do {
result = sandbox.step();
if (result.value?.async) {
await result.value.value;
}
} while (!result.done);
expect(sandbox.context) expect(sandbox.context)
.to.deep.equal({ .to.deep.equal({
foo: 1, foo: 1,
@ -145,10 +147,42 @@ test('evaluates conditional branches', async () => {
}); });
}); });
test('evaluates conditional expressions', async () => {
const sandbox = new Sandbox(
await parse(`
const x = true || false ? 1 : 2
const y = false && true ? 1 : 2
const a = (await true) ? await 1 : await 2
const b = (await false) ? await 1 : await 2
xx = true || false ? 1 : 2
yy = false && true ? 1 : 2
aa = (await true) ? await 1 : await 2
bb = (await false) ? await 1 : await 2
`),
);
await finish(sandbox);
expect(sandbox.context)
.to.deep.equal({
x: 1,
y: 2,
a: 1,
b: 2,
xx: 1,
yy: 2,
aa: 1,
bb: 2,
});
});
test('evaluates loops', async () => { test('evaluates loops', async () => {
const sandbox = new Sandbox( const sandbox = new Sandbox(
await parse(` await parse(`
let x = 0, y = 0, a = 0, b = 0, c = 0; x = 0
y = 0
a = 0
b = 0
c = 0
d = 0
for (let i = 0; i < 3; ++i) { for (let i = 0; i < 3; ++i) {
x += 1; x += 1;
} }
@ -159,25 +193,23 @@ test('evaluates loops', async () => {
a += 1; a += 1;
} while (a < 3); } while (a < 3);
do { do {
b += 1; b += await 1;
} while (await b < 3); } while (await b < 3);
while (c < 3) { while (c < 3) {
c += 1; c += 1;
} }
while (await d < 3) {
d += await 1;
}
`), `),
); );
let result; await finish(sandbox);
do {
result = sandbox.step();
if (result.value?.async) {
await result.value.value;
}
} while (!result.done);
expect(sandbox.context) expect(sandbox.context)
.to.deep.equal({ .to.deep.equal({
a: 3, a: 3,
b: 3, b: 3,
c: 3, c: 3,
d: 3,
x: 3, x: 3,
y: 3, y: 3,
}); });
@ -207,8 +239,20 @@ test('returns values at the top level', async () => {
y = 8 y = 8
`, {allowReturnOutsideFunction: true}), `, {allowReturnOutsideFunction: true}),
); );
const {value: {value}} = sandbox.run(); expect(sandbox.run().value)
expect(value) .to.deep.equal([48, 12]);
expect(sandbox.context)
.to.deep.equal({x: 16, y: 4});
sandbox = new Sandbox(
await parse(`
x = 16
y = 4
return await [x * 3, y * 3]
x = 32
y = 8
`, {allowReturnOutsideFunction: true}),
);
expect((await finish(sandbox)).value)
.to.deep.equal([48, 12]); .to.deep.equal([48, 12]);
expect(sandbox.context) expect(sandbox.context)
.to.deep.equal({x: 16, y: 4}); .to.deep.equal({x: 16, y: 4});
@ -226,6 +270,24 @@ test('returns values at the top level', async () => {
.to.deep.equal({x: 16, y: 4}); .to.deep.equal({x: 16, y: 4});
}); });
test('returns arbitrarily', async () => {
let sandbox;
sandbox = new Sandbox(
await parse(`
x = 16
y = 4
if (x === 16) {
return false
}
`, {allowReturnOutsideFunction: true}),
);
const {value} = sandbox.run();
expect(value)
.to.deep.equal(false);
expect(sandbox.context)
.to.deep.equal({x: 16, y: 4});
});
test('sets variables in global scope', async () => { test('sets variables in global scope', async () => {
const sandbox = new Sandbox( const sandbox = new Sandbox(
await parse(` await parse(`
@ -257,3 +319,133 @@ test('runs arbitrary number of ops', async () => {
expect(true) expect(true)
.to.be.true; .to.be.true;
}); });
test('clears for loop deferred context', async () => {
const context = {x: 10};
const sandbox = new Sandbox(
await parse(`
let l = 0
for (let i = 0; i < x; ++i) {
l += 1
}
`),
context,
);
expect(sandbox.run())
.to.deep.include({
value: 10,
});
context.x = 0
expect(sandbox.run())
.to.deep.include({
value: undefined,
});
});
test('clears while loop deferred context', async () => {
const context = {x: 10};
const sandbox = new Sandbox(
await parse(`
let l = 0
while (l < x) {
l += 1
}
`),
context,
);
expect(sandbox.run())
.to.deep.include({
value: 10,
});
context.x = 0
expect(sandbox.run())
.to.deep.include({
value: undefined,
});
});
test('executes member expressions', async () => {
const sandbox = new Sandbox(
await parse(`
let l = {}
l.f = 54
if (l.f) {
l.g = 65
}
`),
);
expect(sandbox.run())
.to.deep.include({
value: 65,
});
});
test('handles nested yields', async () => {
expect(
await finish(new Sandbox(
await parse(`
for (let i = 0; i < 1; ++i) {
for (let j = 0; j < 1; ++j) {
}
}
`),
))
)
.to.deep.include({value: undefined});
expect(
(
await finish(new Sandbox(
await parse(`
for (let i = 0; i < 1; ++i) {
for (let j = 0; j < 1; ++j) {
await 1;
}
}
`),
))
)
)
.to.deep.include({value: 1});
expect(
(
await finish(new Sandbox(
await parse(`
if (true) {
for (let i = 0; i < 1; ++i) {
}
}
`),
))
)
)
.to.deep.include({value: undefined});
expect(
(
await finish(new Sandbox(
await parse(`
if (1 ? await 2 : 3) {
for (let i = 0; i < 1; ++i) {
}
}
`),
))
)
)
.to.deep.include({value: undefined});
});
test('handles optional expressions', async () => {
const context = {
console,
projected: undefined,
}
const sandbox = new Sandbox(
await parse(`
if (projected?.length > 0) {
}
`),
context,
);
expect(sandbox.run())
.to.deep.include({value: undefined});
});

View File

@ -8,7 +8,7 @@ export default class Scope {
} }
allocate(key, value) { allocate(key, value) {
this.context[key] = value; return this.context[key] = value;
} }
get(key) { get(key) {

View File

@ -15,6 +15,7 @@ export const TRAVERSAL_PATH = {
IfStatement: ['alternate', 'consequent', 'test'], IfStatement: ['alternate', 'consequent', 'test'],
MemberExpression: ['object', 'property'], MemberExpression: ['object', 'property'],
Literal: [], Literal: [],
LogicalExpression: ['left', 'right'],
ObjectExpression: ['properties'], ObjectExpression: ['properties'],
ObjectPattern: ['properties'], ObjectPattern: ['properties'],
Program: ['body'], Program: ['body'],
@ -35,15 +36,9 @@ export default function traverse(node, visitor) {
} }
visitor(node, 'enter'); visitor(node, 'enter');
const path = TRAVERSAL_PATH[node.type]; const path = TRAVERSAL_PATH[node.type];
let children; const children = [];
if (path instanceof Function) { for (const key of path) {
children = path(node); children.push(...(Array.isArray(node[key]) ? node[key] : [node[key]]));
}
else if (Array.isArray(path)) {
children = [];
for (const key of path) {
children.push(...(Array.isArray(node[key]) ? node[key] : [node[key]]));
}
} }
for (const child of children) { for (const child of children) {
if (child) { if (child) {

View File

@ -1,116 +0,0 @@
export function isArrayPattern(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'ArrayPattern') {
return false;
}
return true;
}
export function isBlockStatement(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'BlockStatement') {
return false;
}
return true;
}
export function isDoWhileStatement(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'DoWhileStatement') {
return false;
}
return true;
}
export function isExpressionStatement(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'ExpressionStatement') {
return false;
}
return true;
}
export function isForStatement(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'ForStatement') {
return false;
}
return true;
}
export function isIdentifier(node) {
if (!node || node.type !== 'Identifier') {
return false;
}
return true;
}
export function isIfStatement(node) {
if (!node || node.type !== 'IfStatement') {
return false;
}
return true;
}
export function isLiteral(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'Literal') {
return false;
}
return true;
}
export function isMemberExpression(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'MemberExpression') {
return false;
}
return true;
}
export function isObjectPattern(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'ObjectPattern') {
return false;
}
return true;
}
export function isProperty(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'Property') {
return false;
}
return true;
}
export function isReturnStatement(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'ReturnStatement') {
return false;
}
return true;
}
export function isSpreadElement(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'SpreadElement') {
return false;
}
return true;
}
export function isVariableDeclarator(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'VariableDeclarator') {
return false;
}
return true;
}
export function isWhileStatement(node) {
if (!node || node.type !== 'WhileStatement') {
return false;
}
return true;
}

View File

@ -1,12 +1,17 @@
import Component from '@/ecs/component.js'; import Component from '@/ecs/component.js';
export default class Controlled extends Component { export default class Controlled extends Component {
instanceFromSchema() {
return class ControlledInstance extends super.instanceFromSchema() {
toJSON() {
return {};
}
}
}
static properties = { static properties = {
locked: {type: 'uint8'},
moveUp: {type: 'float32'}, moveUp: {type: 'float32'},
moveRight: {type: 'float32'}, moveRight: {type: 'float32'},
moveDown: {type: 'float32'}, moveDown: {type: 'float32'},
moveLeft: {type: 'float32'}, moveLeft: {type: 'float32'},
changeSlot: {type: 'int8'},
}; };
} }

View File

@ -130,6 +130,15 @@ export default class Engine {
}, },
Water: {water: {}}, Water: {water: {}},
}); });
await ecs.create({
Position: {x: 100, y: 100},
Sprite: {
anchor: {x: 0.5, y: 0.8},
animation: 'shit-shack/shit-shack/0',
source: '/assets/shit-shack/shit-shack.json',
},
VisibleAabb: {},
});
const defaultSystems = [ const defaultSystems = [
'ResetForces', 'ResetForces',
'ApplyControlMovement', 'ApplyControlMovement',
@ -151,7 +160,7 @@ export default class Engine {
System.active = true; System.active = true;
} }
}); });
this.saveEcs(join('homesteads', `${id}`), ecs); await this.saveEcs(join('homesteads', `${id}`), ecs);
} }
async createPlayer(id) { async createPlayer(id) {
@ -184,7 +193,6 @@ export default class Engine {
}, },
Health: {health: 100}, Health: {health: 100},
Position: {x: 368, y: 368}, Position: {x: 368, y: 368},
VisibleAabb: {},
Speed: {speed: 100}, Speed: {speed: 100},
Sound: {}, Sound: {},
Sprite: { Sprite: {
@ -196,6 +204,7 @@ export default class Engine {
speed: 0.115, speed: 0.115,
}, },
Ticking: {}, Ticking: {},
VisibleAabb: {},
Wielder: { Wielder: {
activeSlot: 0, activeSlot: 0,
}, },

View File

@ -10,6 +10,9 @@ class TestServer extends Server {
super(); super();
this.data = {}; this.data = {};
} }
async readAsset() {
return new ArrayBuffer(0);
}
async readData(path) { async readData(path) {
if (path in this.data) { if (path in this.data) {
return this.data[path]; return this.data[path];
@ -34,16 +37,17 @@ test('visibility-based updates', async () => {
Position: {x: (RESOLUTION.x * 1.5) + 32 - 3, y: 20}, Position: {x: (RESOLUTION.x * 1.5) + 32 - 3, y: 20},
VisibleAabb: {}, VisibleAabb: {},
})); }));
const {entity: mainEntity} = engine.connectedPlayers.get(0);
// Tick and get update. Should be a full update. // Tick and get update. Should be a full update.
engine.tick(1); engine.tick(1);
expect(engine.updateFor(0)) expect(engine.updateFor(0))
.to.deep.include({2: {MainEntity: {}, ...ecs.get(2).toJSON()}, 3: ecs.get(3).toJSON()}); .to.deep.include({[mainEntity.id]: {MainEntity: {}, ...ecs.get(mainEntity.id).toJSON()}, [entity.id]: ecs.get(entity.id).toJSON()});
engine.setClean(); engine.setClean();
// Tick and get update. Should be a partial update. // Tick and get update. Should be a partial update.
engine.tick(1); engine.tick(1);
expect(engine.updateFor(0)) expect(engine.updateFor(0))
.to.deep.include({ .to.deep.include({
3: { [entity.id]: {
Position: {x: (RESOLUTION.x * 1.5) + 32 - 1}, Position: {x: (RESOLUTION.x * 1.5) + 32 - 1},
VisibleAabb: { VisibleAabb: {
x0: 1199, x0: 1199,
@ -55,11 +59,11 @@ test('visibility-based updates', async () => {
// Tick and get update. Should remove the entity. // Tick and get update. Should remove the entity.
engine.tick(1); engine.tick(1);
expect(engine.updateFor(0)) expect(engine.updateFor(0))
.to.deep.include({3: false}); .to.deep.include({[entity.id]: false});
// Aim back toward visible area and tick. Should be a full update for that entity. // Aim back toward visible area and tick. Should be a full update for that entity.
engine.setClean(); engine.setClean();
entity.Forces.forceX = -1; entity.Forces.forceX = -1;
engine.tick(1); engine.tick(1);
expect(engine.updateFor(0)) expect(engine.updateFor(0))
.to.deep.include({3: ecs.get(3).toJSON()}); .to.deep.include({[entity.id]: ecs.get(entity.id).toJSON()});
}); });

View File

@ -10,9 +10,6 @@ function onMessage(event) {
onmessage = async (event) => { onmessage = async (event) => {
if (!connected) { if (!connected) {
const url = new URL(`wss://${event.data.host}/ws`) const url = new URL(`wss://${event.data.host}/ws`)
if ('production' === process.env.NODE_ENV) {
url.protocol = 'ws:';
}
socket = new WebSocket(url.href); socket = new WebSocket(url.href);
socket.binaryType = 'arraybuffer'; socket.binaryType = 'arraybuffer';
const {promise, resolve} = Promise.withResolvers(); const {promise, resolve} = Promise.withResolvers();

View File

@ -26,9 +26,6 @@ export default class RemoteClient extends Client {
} }
else { else {
const url = new URL(`wss://${host}/ws`) const url = new URL(`wss://${host}/ws`)
if ('production' === process.env.NODE_ENV) {
url.protocol = 'ws:';
}
this.socket = new WebSocket(url.href); this.socket = new WebSocket(url.href);
this.socket.binaryType = 'arraybuffer'; this.socket.binaryType = 'arraybuffer';
const onMessage = (event) => { const onMessage = (event) => {

View File

@ -1,4 +1,4 @@
import {get, set} from 'idb-keyval'; import {del, get, set} from 'idb-keyval';
import {encode} from '@/packets/index.js'; import {encode} from '@/packets/index.js';
@ -27,6 +27,9 @@ class WorkerServer extends Server {
error.code = 'ENOENT'; error.code = 'ENOENT';
throw error; throw error;
} }
async removeData(path) {
await del(this.constructor.qualify(path));
}
async writeData(path, view) { async writeData(path, view) {
await set(this.constructor.qualify(path), view); await set(this.constructor.qualify(path), view);
} }
@ -38,7 +41,7 @@ const engine = new Engine(WorkerServer);
onmessage = async (event) => { onmessage = async (event) => {
if (0 === event.data) { if (0 === event.data) {
engine.stop(); engine.stop();
await engine.disconnectPlayer(0, 0); await engine.disconnectPlayer(0);
await engine.saveEcses(); await engine.saveEcses();
postMessage(0); postMessage(0);
return; return;
@ -54,8 +57,14 @@ onmessage = async (event) => {
})(); })();
if (import.meta.hot) { if (import.meta.hot) {
import.meta.hot.accept('../engine/engine.js', async () => { import.meta.hot.accept('../../engine.js', async ({default: Engine}) => {
await engine.disconnectPlayer(0, 0); await engine.disconnectPlayer(0);
if (Engine.prototype.createHomestead.toString() !== engine.createHomestead.toString()) {
delete engine.ecses['homesteads/0'];
await engine.server.removeData('homesteads/0');
const newEngine = new Engine(WorkerServer);
await newEngine.createHomestead(0);
}
postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'})); postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'}));
close(); close();
}); });

View File

@ -1,5 +1,13 @@
import {useLoaderData} from '@remix-run/react';
import styles from './index.module.css'; import styles from './index.module.css';
export function loader({request}) {
return {
host: new URL(request.url).host,
};
}
export const meta = () => { export const meta = () => {
return [ return [
{ {
@ -13,12 +21,13 @@ export const meta = () => {
}; };
export default function Index() { export default function Index() {
const {host} = useLoaderData();
return ( return (
<div> <div>
<h1 className={styles.title}>Silphius</h1> <h1 className={styles.title}>Silphius</h1>
<ul className={styles.actions}> <ul className={styles.actions}>
<li><a href="/play/local">Single-player</a></li> <li><a href="/play/local">Single-player</a></li>
<li><a href="/play/remote/localhost:3000">Multi-player</a></li> <li><a href={`/play/remote/${host}`}>Multi-player</a></li>
</ul> </ul>
</div> </div>
); );

View File

@ -77,7 +77,7 @@ export default class Script {
evaluateSync() { evaluateSync() {
this.sandbox.reset(); this.sandbox.reset();
const {value: {value}} = this.sandbox.run(); const {value} = this.sandbox.run();
return value; return value;
} }
@ -113,23 +113,20 @@ export default class Script {
} }
while (true) { while (true) {
this.sandbox.context.elapsed = elapsed; this.sandbox.context.elapsed = elapsed;
const {done, value} = this.sandbox.step(); const {async, done, value} = this.sandbox.step();
if (value) { if (async) {
const {async, value: result} = value; this.promise = value;
if (async) { value
this.promise = result; .catch(reject)
result .then(() => {
.catch(reject) if (done) {
.then(() => { resolve();
if (done) { }
resolve(); })
} .finally(() => {
}) this.promise = null;
.finally(() => { });
this.promise = null; break;
});
break;
}
} }
if (done) { if (done) {
resolve(); resolve();

View File

@ -1,4 +1,4 @@
import {mkdir, readFile, writeFile} from 'node:fs/promises'; import {mkdir, readFile, unlink, writeFile} from 'node:fs/promises';
import {dirname, join} from 'node:path'; import {dirname, join} from 'node:path';
import {WebSocketServer} from 'ws'; import {WebSocketServer} from 'ws';
@ -8,6 +8,8 @@ import {getSession} from '@/session.server.js';
import Engine from './engine.js'; import Engine from './engine.js';
const isInsecure = process.env.SILPHIUS_INSECURE_HTTP;
const wss = new WebSocketServer({ const wss = new WebSocketServer({
noServer: true, noServer: true,
}); });
@ -50,7 +52,7 @@ class SocketServer extends Server {
} }
async readAsset(path) { async readAsset(path) {
const url = new URL(path, 'https://localhost:3000') const url = new URL(path, 'https://localhost:3000')
if ('production' === process.env.NODE_ENV) { if (isInsecure) {
url.protocol = 'http:'; url.protocol = 'http:';
} }
return fetch(url.href).then((response) => ( return fetch(url.href).then((response) => (
@ -62,6 +64,9 @@ class SocketServer extends Server {
await this.ensurePath(dirname(qualified)); await this.ensurePath(dirname(qualified));
return readFile(qualified); return readFile(qualified);
} }
async removeData(path) {
await unlink(path);
}
async writeData(path, view) { async writeData(path, view) {
const qualified = this.constructor.qualify(path); const qualified = this.constructor.qualify(path);
await this.ensurePath(dirname(qualified)); await this.ensurePath(dirname(qualified));

View File

@ -7,7 +7,7 @@
"build": "remix vite:build", "build": "remix vite:build",
"dev": "NODE_OPTIONS=--use-openssl-ca node ./server.js", "dev": "NODE_OPTIONS=--use-openssl-ca node ./server.js",
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"start": "cross-env NODE_ENV=production NODE_OPTIONS=--use-openssl-ca npm run dev", "start": "cross-env NODE_ENV=production npm run dev",
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"storybook:build": "storybook build", "storybook:build": "storybook build",
"test": "vitest app" "test": "vitest app"

View File

@ -2,9 +2,11 @@
"icon": "/assets/hoe/icon.png", "icon": "/assets/hoe/icon.png",
"projectionCheck": "/assets/hoe/projection-check.js", "projectionCheck": "/assets/hoe/projection-check.js",
"projection": { "projection": {
"distance": [1, 0], "distance": [3, -1],
"grid": [ "grid": [
[1] [1, 1, 1],
[1, 1, 1],
[1, 1, 1]
] ]
}, },
"start": "/assets/hoe/start.js" "start": "/assets/hoe/start.js"

View File

@ -118,9 +118,6 @@ if (projected?.length > 0) {
if ([1, 2, 3, 4].includes(layer.tile(projected[i]))) { if ([1, 2, 3, 4].includes(layer.tile(projected[i]))) {
layer.stamp(projected[i], [[7]]) layer.stamp(projected[i], [[7]])
} }
// else if ([6].includes(layer.tile(projected[i]))) {
// layer.stamp(projected[i], [[7]])
// }
} }
Controlled.locked = 0; Controlled.locked = 0;

View File

@ -0,0 +1 @@
{"animations":{"shit-shack/shit-shack/0":["shit-shack/shit-shack/0"]},"frames":{"shit-shack/shit-shack/0":{"frame":{"x":0,"y":0,"w":110,"h":102},"spriteSourceSize":{"x":0,"y":0,"w":110,"h":102},"sourceSize":{"w":110,"h":102}}},"meta":{"format":"RGBA8888","image":"./shit-shack.png","scale":1,"size":{"w":110,"h":102}}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -2,9 +2,11 @@
"icon": "/assets/watering-can/icon.png", "icon": "/assets/watering-can/icon.png",
"projectionCheck": "/assets/watering-can/projection-check.js", "projectionCheck": "/assets/watering-can/projection-check.js",
"projection": { "projection": {
"distance": [1, 0], "distance": [3, -1],
"grid": [ "grid": [
[1] [1, 1, 1],
[1, 1, 1],
[1, 1, 1]
] ]
}, },
"start": "/assets/watering-can/start.js" "start": "/assets/watering-can/start.js"

View File

@ -4,11 +4,12 @@ import express from 'express';
import morgan from 'morgan'; import morgan from 'morgan';
const isProduction = process.env.NODE_ENV === 'production'; const isProduction = process.env.NODE_ENV === 'production';
const isInsecure = process.env.SILPHIUS_INSECURE_HTTP;
const app = express(); const app = express();
let server; let server;
if (isProduction) { if (isInsecure) {
const {createServer} = await import('node:http'); const {createServer} = await import('node:http');
server = createServer(app); server = createServer(app);
} }
@ -39,18 +40,23 @@ const viteDevServer = isProduction
}) })
); );
const ssr = await ( let websocketBuilt = false;
viteDevServer
? viteDevServer.ssrLoadModule('virtual:remix/server-build')
: import('./build/server/index.js')
);
const remixHandler = createRequestHandler({ const remixHandler = createRequestHandler({
build: () => ssr, build: async () => {
const ssr = await (
viteDevServer
? viteDevServer.ssrLoadModule('virtual:remix/server-build')
: import('./build/server/index.js')
);
if (!websocketBuilt) {
await ssr.entry.module.websocket(server, viteDevServer);
websocketBuilt = true;
}
return ssr;
},
}); });
await ssr.entry.module.websocket(server, viteDevServer);
app.use(compression()); app.use(compression());
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header // http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
@ -79,5 +85,5 @@ app.all('*', remixHandler);
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
server.listen(port, () => server.listen(port, () =>
console.log(`Express server listening at http${isProduction ? '' : 's'}://localhost:${port}`) console.log(`Express server listening at http${isInsecure ? '' : 's'}://localhost:${port}`)
); );

View File

@ -6,7 +6,7 @@ import Hotbar from '@/react-components/hotbar.jsx';
import DomDecorator from './dom-decorator.jsx'; import DomDecorator from './dom-decorator.jsx';
const slots = Array(10).fill({}); const slots = Array(10).fill({});
slots[2] = {qty: 24, source: '/assets/potion'}; slots[2] = {qty: 24, icon: '/assets/potion/icon.png'};
export default { export default {
title: 'Dom/Inventory/Hotbar', title: 'Dom/Inventory/Hotbar',

View File

@ -31,7 +31,7 @@ export default {
}, },
}, },
args: { args: {
source: '/assets/potion', icon: '/assets/potion/icon.png',
}, },
}; };