Failed attempt at using method calls to manage awaited method
This commit is contained in:
@@ -1,108 +0,0 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
|
||||
// Actions are creature actions that have been partially executed and not yet resolved
|
||||
// They require some user input to progress
|
||||
let ActiveActions = new Mongo.Collection('activeActions');
|
||||
|
||||
let ActiveActionSchema = new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
// Which creature is taking the action
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
// The user who began taking the action
|
||||
userId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
// Requests for user input
|
||||
questions: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
optional: true,
|
||||
},
|
||||
// User responses
|
||||
answers: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
ActiveActions.attachSchema(ActiveActionSchema);
|
||||
|
||||
export default ActiveActions;
|
||||
|
||||
export const answerAction = new ValidatedMethod({
|
||||
name: 'activeActions.answer',
|
||||
validate: null /*new SimpleSchema({
|
||||
activeActionId: SimpleSchema.RegEx.Id,
|
||||
answers: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
},
|
||||
}).validator()*/,
|
||||
applyOptions: {
|
||||
throwStubExceptions: false,
|
||||
},
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ activeActionId, answers }) {
|
||||
return ActiveActions.update({}, {
|
||||
$set: { answers },
|
||||
$unset: { questions: 1 },
|
||||
});
|
||||
const action = ActiveActions.findOne(activeActionId);
|
||||
// Permissions
|
||||
if (action.userId !== this.userId) {
|
||||
throw new Meteor.Error('Permission denied', 'You do not own this action');
|
||||
}
|
||||
return ActiveActions.update(activeActionId, {
|
||||
$set: { answers },
|
||||
$unset: { questions: 1 },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const removeAction = new ValidatedMethod({
|
||||
name: 'activeActions.remove',
|
||||
validate: new SimpleSchema({
|
||||
activeActionId: SimpleSchema.RegEx.Id,
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ activeActionId }) {
|
||||
const action = ActiveActions.findOne(activeActionId);
|
||||
// Permissions
|
||||
if (action.userId !== this.userId) {
|
||||
throw new Meteor.Error('Permission denied', 'You do not own this action');
|
||||
}
|
||||
return ActiveActions.remove(activeActionId);
|
||||
},
|
||||
});
|
||||
|
||||
// TODO remove this
|
||||
export const removeAllActions = new ValidatedMethod({
|
||||
name: 'activeActions.removeAll',
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run() {
|
||||
return ActiveActions.remove({});
|
||||
},
|
||||
});
|
||||
@@ -5,11 +5,12 @@ import {
|
||||
import { groupBy, remove } from 'lodash';
|
||||
|
||||
export default class ActionContext {
|
||||
constructor(creatureId, targetIds = [], method, activeActionId) {
|
||||
constructor(creatureId, targetIds = [], method, invocationId) {
|
||||
// Get the creature
|
||||
this.creature = getCreature(creatureId)
|
||||
// Store an active action ID for pausing/resuming this action
|
||||
this.activeActionId = activeActionId
|
||||
// Store the details for pausing for user interaction
|
||||
this.invocationId = invocationId;
|
||||
this.userInputStep = 0;
|
||||
|
||||
if (!this.creature) {
|
||||
throw new Meteor.Error('No Creature', `No creature could be found with id: ${creatureId}`)
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function applyDamage(node, actionContext) {
|
||||
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
|
||||
|
||||
// roll the dice only and store that string
|
||||
recalculateCalculation(prop.amount, actionContext, undefined, 'compile');
|
||||
recalculateCalculation(prop.amount, actionContext, 'compile');
|
||||
const { result: rolled } = resolve('roll', prop.amount.valueNode, scope, context);
|
||||
if (rolled.parseType !== 'constant') {
|
||||
logValue.push(toString(rolled));
|
||||
|
||||
@@ -10,7 +10,6 @@ import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import applyProperty from './applyProperty.js';
|
||||
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
|
||||
import ActiveActions from '/imports/api/creature/actions/ActiveActions';
|
||||
|
||||
const doAction = new ValidatedMethod({
|
||||
name: 'creatureProperties.doAction',
|
||||
@@ -31,24 +30,27 @@ const doAction = new ValidatedMethod({
|
||||
blackbox: true,
|
||||
optional: true,
|
||||
},
|
||||
invocationId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
optional: true,
|
||||
}
|
||||
}).validator(),
|
||||
applyOptions: {
|
||||
throwStubExceptions: false,
|
||||
},
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
async run({ actionId, targetIds = [], scope }) {
|
||||
async run({ actionId, targetIds = [], scope, invocationId }) {
|
||||
console.log('do Action running');
|
||||
// Get action context
|
||||
let action = CreatureProperties.findOne(actionId);
|
||||
const creatureId = action.ancestors[0].id;
|
||||
// TODO remove this
|
||||
// For testing, remove all other active actions before inserting this one
|
||||
ActiveActions.remove({});
|
||||
const activeActionId = await ActiveActions.insertAsync({
|
||||
creatureId,
|
||||
userId: this.userId,
|
||||
});
|
||||
const actionContext = new ActionContext(creatureId, targetIds, this, activeActionId);
|
||||
|
||||
const actionContext = new ActionContext(creatureId, targetIds, this, invocationId);
|
||||
|
||||
// Check permissions
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
@@ -67,11 +69,13 @@ const doAction = new ValidatedMethod({
|
||||
await doActionWork({ properties, ancestors, actionContext, methodScope: scope });
|
||||
|
||||
// Recompute all involved creatures
|
||||
Creatures.update({
|
||||
_id: { $in: [creatureId, ...targetIds] }
|
||||
}, {
|
||||
$set: { dirty: true },
|
||||
});
|
||||
if (Meteor.isServer) {
|
||||
Creatures.updateAsync({
|
||||
_id: { $in: [creatureId, ...targetIds] }
|
||||
}, {
|
||||
$set: { dirty: true },
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,28 +1,90 @@
|
||||
import ActiveActions from '/imports/api/creature/actions/ActiveActions';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import { set } from 'lodash';
|
||||
|
||||
// Reminder: throwStubExceptions: true is the default, and only
|
||||
// possible when run() is not async
|
||||
// For async run() stub exceptions never stop the client from sending
|
||||
// the call to the server
|
||||
|
||||
// Dict of invocationId: {steps: {earlyAnswers, resolve, reject}}
|
||||
// either resolve functions waiting for the user's input or early answers that were provided
|
||||
// before the resolves could be set up
|
||||
let userInputRequests = {};
|
||||
let provideUserInput;
|
||||
|
||||
if (Meteor.isClient) {
|
||||
provideUserInput = function (invocationId, step, answers, callback) {
|
||||
Meteor.call('answerUserInputRequest', { invocationId, step, answers }, callback);
|
||||
// Do the same work on the client without using a stub
|
||||
answerInputRequestWork({ invocationId, step, answers });
|
||||
}
|
||||
}
|
||||
|
||||
export { userInputRequests, provideUserInput };
|
||||
|
||||
export default async function getUserInput(questions, actionContext) {
|
||||
const activeActionId = actionContext.activeActionId;
|
||||
// Set the questions on the active action
|
||||
ActiveActions.update(activeActionId, {
|
||||
$set: { questions },
|
||||
$unset: { answers: 1 },
|
||||
});
|
||||
// Wait for answers
|
||||
// get the invocation details from the action context
|
||||
const invocationId = actionContext.invocationId;
|
||||
const step = actionContext.userInputStep;
|
||||
actionContext.userInputStep += 1; // increment userInput step every time
|
||||
|
||||
// If the answers are already waiting, just return them
|
||||
if (userInputRequests[invocationId]?.[step]?.earlyAnswers) {
|
||||
return userInputRequests[invocationId][step].earlyAnswers;
|
||||
}
|
||||
// On the client, store the questions to be answered
|
||||
if (Meteor.isClient) {
|
||||
set(userInputRequests, `${invocationId}[${step}]`, { questions });
|
||||
}
|
||||
// Create a place for the answers to go when they are provided
|
||||
return new Promise((resolve, reject) => {
|
||||
const observerHandle = ActiveActions.find({
|
||||
_id: activeActionId
|
||||
}).observeChanges({
|
||||
changed(id, fields) {
|
||||
// Only watch for answers
|
||||
if (!fields.answers) return;
|
||||
// Stop watching
|
||||
observerHandle.stop();
|
||||
// Give answers
|
||||
resolve(fields.answers);
|
||||
},
|
||||
removed() {
|
||||
reject('Active action was deleted')
|
||||
},
|
||||
});
|
||||
set(userInputRequests, `${invocationId}[${step}]`, { resolve, reject });
|
||||
});
|
||||
}
|
||||
|
||||
function answerInputRequestWork({ invocationId, step, answers }) {
|
||||
console.log('running answerUserInputRequest');
|
||||
const invocation = userInputRequests[invocationId];
|
||||
if (!invocation) {
|
||||
// Call order on the server is guaranteed, so the invocation must have been created
|
||||
// Before we can update it
|
||||
throw new Meteor.Error('Not found', 'The method this answer is updating does not exist');
|
||||
}
|
||||
if (invocation[step]?.resolve) {
|
||||
// If there is a resolve waiting for this response, provide it
|
||||
invocation[step].resolve(answers);
|
||||
} else {
|
||||
// Otherwise just store the response as early answers
|
||||
invocation[step] = {
|
||||
earlyAnswers: answers
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (Meteor.isServer) {
|
||||
// This function is not defined on the client so that it has no stub function
|
||||
// This allows it to be called while still simulating an awaited async method
|
||||
// See https://guide.meteor.com/2.8-migration.html#the-limitations
|
||||
new ValidatedMethod({
|
||||
name: 'answerUserInputRequest',
|
||||
validate: new SimpleSchema({
|
||||
invocationId: SimpleSchema.RegEx.Id,
|
||||
step: SimpleSchema.Integer,
|
||||
answers: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
},
|
||||
}).validator(),
|
||||
applyOptions: {
|
||||
throwStubExceptions: false,
|
||||
},
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 20,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run: answerInputRequestWork,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<template lang="html">
|
||||
<dialog-base>
|
||||
<template slot="toolbar">
|
||||
<v-toolbar-title>
|
||||
Action
|
||||
</v-toolbar-title>
|
||||
</template>
|
||||
<div>
|
||||
content
|
||||
</div>
|
||||
<v-btn
|
||||
slot="actions"
|
||||
text
|
||||
@click="cancel"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-spacer slot="actions" />
|
||||
<v-btn
|
||||
slot="actions"
|
||||
text
|
||||
@click="apply(true)"
|
||||
>
|
||||
Step
|
||||
</v-btn>
|
||||
<v-btn
|
||||
slot="actions"
|
||||
text
|
||||
@click="apply()"
|
||||
>
|
||||
Apply all
|
||||
</v-btn>
|
||||
</dialog-base>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
|
||||
import doAction from '/imports/api/engine/actions/doAction';
|
||||
import { provideUserInput } from '/imports/api/engine/actions/getUserInput.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
},
|
||||
props: {
|
||||
propId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
invocationId: undefined,
|
||||
answers: {},
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
|
||||
},
|
||||
methods: {
|
||||
apply(step = false) {
|
||||
if (this.invocationId) {
|
||||
provideUserInput(this.invocationId, 0, { index: 1 });
|
||||
} else {
|
||||
this.invocationId = Random.id();
|
||||
doAction.call({
|
||||
invocationId: this.invocationId,
|
||||
actionId: this.propId,
|
||||
targetIds: [],
|
||||
});
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
this.$store.dispatch('popDialogStack');
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Load commonly used dialogs immediately
|
||||
import ActionDialog from '/imports/client/ui/creature/actions/ActionDialog.vue';
|
||||
import InsertPropertyDialog from '/imports/client/ui/properties/InsertPropertyDialog.vue';
|
||||
import CharacterCreationDialog from '/imports/client/ui/creature/character/CharacterCreationDialog.vue';
|
||||
import CastSpellWithSlotDialog from '/imports/client/ui/properties/components/spells/CastSpellWithSlotDialog.vue';
|
||||
@@ -33,6 +34,7 @@ const ShareDialog = () => import('/imports/client/ui/sharing/ShareDialog.vue');
|
||||
const UsernameDialog = () => import('/imports/client/ui/user/UsernameDialog.vue');
|
||||
|
||||
export default {
|
||||
ActionDialog,
|
||||
InsertPropertyDialog,
|
||||
ArchiveDialog,
|
||||
CastSpellWithSlotDialog,
|
||||
|
||||
@@ -187,7 +187,16 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
doAction() {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'action-dialog',
|
||||
elementId: 'do-action-button',
|
||||
data: {
|
||||
propId: this.model._id,
|
||||
},
|
||||
});
|
||||
return;
|
||||
if (this.model.type === 'action') {
|
||||
this.doActionLoading = true;
|
||||
doAction.call({ actionId: this.model._id }, error => {
|
||||
|
||||
@@ -5,51 +5,51 @@ import rollDice from '/imports/parser/rollDice.js';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
|
||||
const rollNode = {
|
||||
create({left, right}) {
|
||||
create({ left, right }) {
|
||||
return {
|
||||
parseType: 'roll',
|
||||
left,
|
||||
right,
|
||||
};
|
||||
},
|
||||
compile(node, scope, context){
|
||||
const {result: left} = resolve('compile', node.left, scope, context);
|
||||
const {result: right} = resolve('compile', node.right, scope, context);
|
||||
compile(node, scope, context) {
|
||||
const { result: left } = resolve('compile', node.left, scope, context);
|
||||
const { result: right } = resolve('compile', node.right, scope, context);
|
||||
return {
|
||||
result: rollNode.create({left, right}),
|
||||
result: rollNode.create({ left, right }),
|
||||
context,
|
||||
};
|
||||
},
|
||||
toString(node){
|
||||
toString(node) {
|
||||
if (
|
||||
node.left.valueType === 'number' && node.left.value === 1
|
||||
){
|
||||
) {
|
||||
return `d${toString(node.right)}`;
|
||||
} else {
|
||||
return `${toString(node.left)}d${toString(node.right)}`;
|
||||
}
|
||||
},
|
||||
roll(node, scope, context){
|
||||
const {result: left} = resolve('reduce', node.left, scope, context);
|
||||
const {result: right} = resolve('reduce', node.right, scope, context);
|
||||
if (left.valueType !== 'number' && !Number.isInteger(left.value)){
|
||||
roll(node, scope, context) {
|
||||
const { result: left } = resolve('reduce', node.left, scope, context);
|
||||
const { result: right } = resolve('reduce', node.right, scope, context);
|
||||
if (left.valueType !== 'number' && !Number.isInteger(left.value)) {
|
||||
return errorResult('Number of dice is not an integer', node, context);
|
||||
}
|
||||
if (right.valueType !== 'number' && !Number.isInteger(right.value)){
|
||||
if (right.valueType !== 'number' && !Number.isInteger(right.value)) {
|
||||
return errorResult('Dice size is not an integer', node, context);
|
||||
}
|
||||
let number = left.value;
|
||||
if (context.options.doubleRolls){
|
||||
if (context.options.doubleRolls) {
|
||||
number *= 2;
|
||||
}
|
||||
if (number > STORAGE_LIMITS.diceRollValuesCount){
|
||||
if (number > STORAGE_LIMITS.diceRollValuesCount) {
|
||||
const message = `Can't roll more than ${STORAGE_LIMITS.diceRollValuesCount} dice at once`;
|
||||
return errorResult(message, node, context);
|
||||
}
|
||||
let diceSize = right.value;
|
||||
let values = rollDice(number, diceSize);
|
||||
if (context){
|
||||
context.rolls.push({number, diceSize, values});
|
||||
if (context) {
|
||||
context.rolls.push({ number, diceSize, values });
|
||||
}
|
||||
return {
|
||||
result: rollArray.create({
|
||||
@@ -60,18 +60,18 @@ const rollNode = {
|
||||
context
|
||||
};
|
||||
},
|
||||
reduce(node, scope, context){
|
||||
const {result} = rollNode.roll(node, scope, context);
|
||||
reduce(node, scope, context) {
|
||||
const { result } = rollNode.roll(node, scope, context);
|
||||
return resolve('reduce', result, scope, context);
|
||||
},
|
||||
traverse(node, fn){
|
||||
traverse(node, fn) {
|
||||
fn(node);
|
||||
traverse(node.left, fn);
|
||||
traverse(node.right, fn);
|
||||
},
|
||||
map(node, fn){
|
||||
map(node, fn) {
|
||||
const resultingNode = fn(node);
|
||||
if (resultingNode === node){
|
||||
if (resultingNode === node) {
|
||||
node.left = map(node.left, fn);
|
||||
node.right = map(node.right, fn);
|
||||
}
|
||||
@@ -79,7 +79,7 @@ const rollNode = {
|
||||
},
|
||||
}
|
||||
|
||||
function errorResult(message, node, context){
|
||||
function errorResult(message, node, context) {
|
||||
context.error(message);
|
||||
return {
|
||||
result: error.create({ node, error: message }),
|
||||
|
||||
@@ -6,7 +6,6 @@ import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
||||
import VERSION from '/imports/constants/VERSION.js';
|
||||
import ActiveActions from '/imports/api/creature/actions/ActiveActions';
|
||||
import { loadCreature } from '/imports/api/engine/loadCreatures.js';
|
||||
|
||||
let schema = new SimpleSchema({
|
||||
@@ -55,11 +54,6 @@ Meteor.publish('singleCharacter', function (creatureId) {
|
||||
limit: 20,
|
||||
sort: { date: -1 },
|
||||
}),
|
||||
ActiveActions.find({
|
||||
creatureId,
|
||||
}, {
|
||||
limit: 10,
|
||||
}),
|
||||
// Also publish the owner's username
|
||||
Meteor.users.find(permissionCreature.owner, {
|
||||
fields: {
|
||||
|
||||
Reference in New Issue
Block a user