# Events

## Event Object

Events are non-extensible, script-only accessible un-managed objects that can trigger events and notifications delayed or on a recurring schedule.

When fired, events move into the `processing` state, and then into the `completed` or `failed` state once handled.

## Object Properties

* `count` { Number *readOnly* } If recurring, this property will hold the number of times the event was triggered, whether manually or through scheduling.
* `err` { Any *readOnly* } A fault that is recorded if the event handler failed for any reason, threw an error, etc.
* `expiresAt` { Date *indexed*, *removable* } If set, the event will not trigger once expired and will eventually be garbage collected and deleted by cortex.
* `if` { Expression *removable* } An optional expression that triggers the event if it evaluates to a *truey* value (`!!if()`).
* `key` { String *indexed*, *unique*, *removable* } An optional unique key up to 512 characters to uniquely identify the event.
* `parent` { ObjectID *readOnly* } Set to the `_id` of a triggering recurring event.
* `parentKey` { String *readOnly*, *indexed* } Set to the `key` of a triggering recurring event.
* `principal` { ObjectID } An account or service account used as the calling principal. Can be set using keys and email addresses.
* `retention` { Number = *never* } The retention policy bits. Only affect non-recurring events and children of recurring events.
  * never = 0 - delete the event after it triggers.
  * failed = 1 - retain events that fail.
  * completed = 2 - retain events that complete.
  * skipped = 4 - retain events that were skipped via conditional.         &#x20;
* `schedule` { String } A cron format recurrence schedule. Once set, the `state` is set to `scheduled` and the `start` property is set to the next time in the schedule. Recurring events cannot be changed to non-recurring events and vice versa.
* `start` { Date = *new Date()* } The date to trigger the event.
* `started` { Date *readOnly* } The date the event processing was started (or last started int he case of a recurring event).
* `state` { Number *indexed*, *readOnly* } The current state
  * scheduled = 0 - event is recurring.
  * pending = 1 - event is scheduled to run.
  * processing = 2 - event is currently processing.
  * completed = 3 - event triggered successfully.
  * failed = 4 - an error occurred during processing and was stored in `err`.&#x20;
  * skipped = 5 - skipped because a condition was not met.

## Object Types

### Script

A `script` event trigger an `@on` runtime event based on the `event` and `param` properties, mirroring the `script.fire()` functionality. The `param` value becomes the `script.arguments` and is also passed as the first argument to the event handler.

* `event` { String } The event name that matches a registered runtime event (`@on`).&#x20;
* `param` { Any } An object passed to the event handler as the first argument.&#x20;

```javascript
const { on } = require('decorators')                          

class Events {           

  @on('ns__test')
  static nsTest(param, runtime) {
    // runtime.source === 'event' when fried from an event instance
  }
}
```

### Notification

A `notification` event sends a notification based on the `name`, `variables` and `options` properties, mirroring the `require('notifications').send(...)` script functionality.

Because notifications can have multiple endpoints, if more than one error is produced, any additional endpoint errors are added as child faults to the first error.

* `name` { String } Notification name. Can be null or missing for custom notifications.&#x20;
* `variables` { Any } Template variables.
* `options` { Any } Notification options.&#x20;

### Driver

A `driver` event runs a db operation based on the `options` and `privileged` properties, mirroring the `require('db.util').createOperation(options, [options]).execute()` script functionality.

Supported operations are: `deleteMany`, `deleteOne`, `insertMany`, `insertOne`, `patchMany`, `patchOne`, `updateMany`, `updateOne`.

* `options` { Object } Operation options from `operation.getOptions()`.&#x20;
* `privileged` { Boolean = *false* } If true, privileged options (those only available in-script)

  will be included (skipAcl, roles, grant, bypassCreateAcl).

### Console

For testing purposes, a `console` event outputs `param` as if `console.log(param)` were called from a script. Console logging is not available in production environments.

* `param` { Any } Max 8KB message to be output.&#x20;

## Constants

```javascript
return consts.events

/*
{
  "retention": {
    "never": 0,
    "failed": 1,
    "completed": 2,        
    "skipped": 4
  },
  "states": {
    "scheduled": 0,    
    "pending": 1,
    "processing": 2,
    "completed": 3,
    "failed": 4,
    "skipped": 5
    }
  }
*/
```

## Scheduling

To schedule a user event, simply insert an event instance.

When a recurring event - one with a `schedule` cron - is triggered, it will insert a new event with a `pending` state with a `start` date set to the current time, and tick up the `count` property of the parent.

## Error Handling

A `err.events.failed` trigger is called when an event fails. The trigger `context` is the event instance, with an additional `params` (script.arguments) object containing the `err` object.

```javascript
const { trigger } = require('decorators')

class ErrorHandling {

  @trigger('err.events.failed')
  handleError({ context, params: { err }}) {

  }

}
```

## Limits

* `configuration.events.softLimit` ( Number = *80,000*, *readOnly* ) The number of pending events that can be inserted before the `err.events.softLimitExceeded` trigger is fired. When fired, `configuration.events.triggerSoftLimit` is set to `false` and will not trigger again until reset.
* `configuration.events.hardLimit` ( Number = *100,000*, *readOnly* ) The number of pending events that can be inserted before the `err.events.hardLimitExceeded` trigger is fired. When fired, `configuration.events.triggerHardLimit` is set to `false` and will not trigger again until reset. When this threshold is reached, a `cortex.invalidArgument.maxAllowed` Fault is thrown.
* `configuration.events.triggerSoftLimit` ( Boolean = *true* ) Initially true, this is set to false if the soft limit is triggered and a script trigger exists in the environment runtime. The trigger will not fire again until the property is reset to `true`. The trigger is *always* fired inline.
* `configuration.events.triggerHardLimit` ( Boolean = *true* ) Initially true, this is set to false if the hard limit is triggered and a script trigger exists in the environment runtime. The trigger will not fire again until the property is reset to `true`. The trigger is *always* fired inline.

```javascript
// listen for limit triggers

const { trigger } = require('decorators')

class Limits {

  @trigger('err.events.softLimitExceeded')
  onSoftLimit() {
    // time to take a look.
  }

  @trigger('err.events.hardLimitExceeded')
  onHardLimit() {
    // this is an error. events won't insert or recur.    
  }
}
```

## Examples

```javascript
const { Event } = org.objects,
      { events: { retention: { failed, skipped } } } = consts

// insert a script event to trigger as soon as possible. and retain only if failed or skipped.
Event.insertOne(
  {
    type: 'script',  
    key: 'unique.key',    
    event: 'c_room_room_event',  
    param: {
      anything: 'up to 16kb'
    },
    if: {
      $eq: ['principal._id', script.principal._id]
    },
    retention: failed | skipped
  }
).grant('update').bypassCreateAcl().execute()

// insert a script event to trigger in 24 hours
Event.insertOne(
  {
    type: 'script',  
    key: 'unique.key',    
    event: 'c_room_room_event',  
    param: {
      anything: 'up to 16kb'
    },
    start: new Date(Date.now() + 86400000)
  }
).grant('update').bypassCreateAcl().execute()

// insert a custom notification to be sent once a day at 5am UTC, running as the current principal.
Event.insertOne(
  {
    type: 'notification',
    key: 'scheduled.notif.123',
    principal: script.principal,
    schedule: '0 5 * * *',
    options: {
      endpoints: {
        email: {
          recipients: [          
            'test@example.org',
            ],
          subject: 'Medable',
          message: 'Medable rocks!', // message (plain text) is currently required.
          html: '<html><p>Medable&nbsp;rocks&#33;<p></html>'
        }
      }
    }
  }
).grant('update').bypassCreateAcl().execute()     

// insert a console logger that will run every minute and expire in 5 minutes.
// then, force it to run immediately after scheduling (any recurring event can be 
// set to run manually, but this resets the next time it will run).
Event.insertOne(
  {      
    type: 'console',       
    param: 'foo',
    key: 'scheduled will expire in 5 minutes',
    schedule: '* * * * *',
    expiresAt: new Date(Date.now() + (1000 * 60 * 5))
  }
).grant('update').bypassCreateAcl().execute()


Event.updateOne(
  {
    key: 'scheduled will expire in 5 minutes'
  },
  {
    $set: {
      start: new Date()
    }
  }
).skipAcl().grant('update').execute()
```
