@metamask/snaps-controllers 12.3.1 → 13.0.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 (88) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/dist/cronjob/CronjobController.cjs +250 -276
  3. package/dist/cronjob/CronjobController.cjs.map +1 -1
  4. package/dist/cronjob/CronjobController.d.cts +61 -78
  5. package/dist/cronjob/CronjobController.d.cts.map +1 -1
  6. package/dist/cronjob/CronjobController.d.mts +61 -78
  7. package/dist/cronjob/CronjobController.d.mts.map +1 -1
  8. package/dist/cronjob/CronjobController.mjs +251 -277
  9. package/dist/cronjob/CronjobController.mjs.map +1 -1
  10. package/dist/cronjob/utils.cjs +79 -0
  11. package/dist/cronjob/utils.cjs.map +1 -0
  12. package/dist/cronjob/utils.d.cts +25 -0
  13. package/dist/cronjob/utils.d.cts.map +1 -0
  14. package/dist/cronjob/utils.d.mts +25 -0
  15. package/dist/cronjob/utils.d.mts.map +1 -0
  16. package/dist/cronjob/utils.mjs +75 -0
  17. package/dist/cronjob/utils.mjs.map +1 -0
  18. package/dist/insights/SnapInsightsController.cjs +199 -149
  19. package/dist/insights/SnapInsightsController.cjs.map +1 -1
  20. package/dist/insights/SnapInsightsController.mjs +198 -148
  21. package/dist/insights/SnapInsightsController.mjs.map +1 -1
  22. package/dist/interface/SnapInterfaceController.cjs +160 -101
  23. package/dist/interface/SnapInterfaceController.cjs.map +1 -1
  24. package/dist/interface/SnapInterfaceController.mjs +160 -101
  25. package/dist/interface/SnapInterfaceController.mjs.map +1 -1
  26. package/dist/multichain/MultichainRouter.cjs +117 -114
  27. package/dist/multichain/MultichainRouter.cjs.map +1 -1
  28. package/dist/multichain/MultichainRouter.mjs +117 -114
  29. package/dist/multichain/MultichainRouter.mjs.map +1 -1
  30. package/dist/services/AbstractExecutionService.cjs +131 -139
  31. package/dist/services/AbstractExecutionService.cjs.map +1 -1
  32. package/dist/services/AbstractExecutionService.mjs +131 -139
  33. package/dist/services/AbstractExecutionService.mjs.map +1 -1
  34. package/dist/services/ProxyPostMessageStream.cjs +19 -26
  35. package/dist/services/ProxyPostMessageStream.cjs.map +1 -1
  36. package/dist/services/ProxyPostMessageStream.mjs +19 -26
  37. package/dist/services/ProxyPostMessageStream.mjs.map +1 -1
  38. package/dist/services/iframe/IframeExecutionService.cjs +1 -0
  39. package/dist/services/iframe/IframeExecutionService.cjs.map +1 -1
  40. package/dist/services/iframe/IframeExecutionService.mjs +1 -0
  41. package/dist/services/iframe/IframeExecutionService.mjs.map +1 -1
  42. package/dist/services/offscreen/OffscreenExecutionService.cjs +3 -16
  43. package/dist/services/offscreen/OffscreenExecutionService.cjs.map +1 -1
  44. package/dist/services/offscreen/OffscreenExecutionService.mjs +3 -16
  45. package/dist/services/offscreen/OffscreenExecutionService.mjs.map +1 -1
  46. package/dist/services/proxy/ProxyExecutionService.cjs +4 -17
  47. package/dist/services/proxy/ProxyExecutionService.cjs.map +1 -1
  48. package/dist/services/proxy/ProxyExecutionService.mjs +4 -17
  49. package/dist/services/proxy/ProxyExecutionService.mjs.map +1 -1
  50. package/dist/services/webview/WebViewExecutionService.cjs +6 -19
  51. package/dist/services/webview/WebViewExecutionService.cjs.map +1 -1
  52. package/dist/services/webview/WebViewExecutionService.mjs +6 -19
  53. package/dist/services/webview/WebViewExecutionService.mjs.map +1 -1
  54. package/dist/services/webview/WebViewMessageStream.cjs +13 -26
  55. package/dist/services/webview/WebViewMessageStream.cjs.map +1 -1
  56. package/dist/services/webview/WebViewMessageStream.mjs +13 -26
  57. package/dist/services/webview/WebViewMessageStream.mjs.map +1 -1
  58. package/dist/snaps/SnapController.cjs +1370 -1161
  59. package/dist/snaps/SnapController.cjs.map +1 -1
  60. package/dist/snaps/SnapController.d.cts +4 -4
  61. package/dist/snaps/SnapController.d.cts.map +1 -1
  62. package/dist/snaps/SnapController.d.mts +4 -4
  63. package/dist/snaps/SnapController.d.mts.map +1 -1
  64. package/dist/snaps/SnapController.mjs +1370 -1161
  65. package/dist/snaps/SnapController.mjs.map +1 -1
  66. package/dist/snaps/Timer.cjs +4 -0
  67. package/dist/snaps/Timer.cjs.map +1 -1
  68. package/dist/snaps/Timer.mjs +4 -0
  69. package/dist/snaps/Timer.mjs.map +1 -1
  70. package/dist/snaps/location/http.cjs +20 -4
  71. package/dist/snaps/location/http.cjs.map +1 -1
  72. package/dist/snaps/location/http.mjs +20 -4
  73. package/dist/snaps/location/http.mjs.map +1 -1
  74. package/dist/snaps/location/local.cjs +4 -17
  75. package/dist/snaps/location/local.cjs.map +1 -1
  76. package/dist/snaps/location/local.mjs +4 -17
  77. package/dist/snaps/location/local.mjs.map +1 -1
  78. package/dist/snaps/location/npm.cjs +28 -48
  79. package/dist/snaps/location/npm.cjs.map +1 -1
  80. package/dist/snaps/location/npm.d.cts.map +1 -1
  81. package/dist/snaps/location/npm.d.mts.map +1 -1
  82. package/dist/snaps/location/npm.mjs +28 -48
  83. package/dist/snaps/location/npm.mjs.map +1 -1
  84. package/dist/snaps/registry/json.cjs +173 -166
  85. package/dist/snaps/registry/json.cjs.map +1 -1
  86. package/dist/snaps/registry/json.mjs +172 -165
  87. package/dist/snaps/registry/json.mjs.map +1 -1
  88. package/package.json +9 -9
@@ -1,122 +1,71 @@
1
- var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
- if (kind === "m") throw new TypeError("Private method is not writable");
3
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
- return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
- };
7
- var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
- };
12
- var _CronjobController_instances, _CronjobController_dailyTimer, _CronjobController_timers, _CronjobController_snapIds, _CronjobController_getAllJobs, _CronjobController_getSnapJobs, _CronjobController_schedule, _CronjobController_executeCronjob, _CronjobController_setUpBackgroundEvent, _CronjobController_updateJobLastRunState, _CronjobController_rescheduleBackgroundEvents;
13
1
  import { BaseController } from "@metamask/base-controller";
14
2
  import { getCronjobCaveatJobs, SnapEndowments } from "@metamask/snaps-rpc-methods";
15
- import { HandlerType, parseCronExpression, logError, logWarning, toCensoredISO8601String } from "@metamask/snaps-utils";
16
- import { assert, Duration, hasProperty, inMilliseconds } from "@metamask/utils";
3
+ import { toCensoredISO8601String, HandlerType, logError } from "@metamask/snaps-utils";
4
+ import { assert, Duration, inMilliseconds } from "@metamask/utils";
17
5
  import { castDraft } from "immer";
18
6
  import { DateTime } from "luxon";
19
7
  import { nanoid } from "nanoid";
20
- import { getRunnableSnaps } from "../index.mjs";
8
+ import { getCronjobSpecificationSchedule, getExecutionDate } from "./utils.mjs";
9
+ import { METAMASK_ORIGIN } from "../snaps/constants.mjs";
21
10
  import { Timer } from "../snaps/Timer.mjs";
22
11
  export const DAILY_TIMEOUT = inMilliseconds(24, Duration.Hour);
23
12
  const controllerName = 'CronjobController';
24
13
  /**
25
- * Use this controller to register and schedule periodically executed jobs
26
- * using RPC method hooks.
14
+ * The cronjob controller is responsible for managing cronjobs and background
15
+ * events for Snaps. It allows Snaps to schedule events that will be executed
16
+ * at a later time.
27
17
  */
28
18
  export class CronjobController extends BaseController {
19
+ #timers;
20
+ #dailyTimer = new Timer(DAILY_TIMEOUT);
29
21
  constructor({ messenger, state }) {
30
22
  super({
31
23
  messenger,
32
24
  metadata: {
33
- jobs: { persist: true, anonymous: false },
34
25
  events: { persist: true, anonymous: false },
35
26
  },
36
27
  name: controllerName,
37
28
  state: {
38
- jobs: {},
39
29
  events: {},
40
30
  ...state,
41
31
  },
42
32
  });
43
- _CronjobController_instances.add(this);
44
- _CronjobController_dailyTimer.set(this, void 0);
45
- _CronjobController_timers.set(this, void 0);
46
- // Mapping from jobId to snapId
47
- _CronjobController_snapIds.set(this, void 0);
48
- __classPrivateFieldSet(this, _CronjobController_timers, new Map(), "f");
49
- __classPrivateFieldSet(this, _CronjobController_snapIds, new Map(), "f");
50
- this._handleSnapRegisterEvent = this._handleSnapRegisterEvent.bind(this);
51
- this._handleSnapUnregisterEvent =
52
- this._handleSnapUnregisterEvent.bind(this);
53
- this._handleEventSnapUpdated = this._handleEventSnapUpdated.bind(this);
54
- this._handleSnapDisabledEvent = this._handleSnapDisabledEvent.bind(this);
55
- this._handleSnapEnabledEvent = this._handleSnapEnabledEvent.bind(this);
56
- // Subscribe to Snap events
57
- /* eslint-disable @typescript-eslint/unbound-method */
58
- this.messagingSystem.subscribe('SnapController:snapInstalled', this._handleSnapRegisterEvent);
59
- this.messagingSystem.subscribe('SnapController:snapUninstalled', this._handleSnapUnregisterEvent);
60
- this.messagingSystem.subscribe('SnapController:snapEnabled', this._handleSnapEnabledEvent);
61
- this.messagingSystem.subscribe('SnapController:snapDisabled', this._handleSnapDisabledEvent);
62
- this.messagingSystem.subscribe('SnapController:snapUpdated', this._handleEventSnapUpdated);
63
- /* eslint-enable @typescript-eslint/unbound-method */
64
- this.messagingSystem.registerActionHandler(`${controllerName}:scheduleBackgroundEvent`, (...args) => this.scheduleBackgroundEvent(...args));
65
- this.messagingSystem.registerActionHandler(`${controllerName}:cancelBackgroundEvent`, (...args) => this.cancelBackgroundEvent(...args));
66
- this.messagingSystem.registerActionHandler(`${controllerName}:getBackgroundEvents`, (...args) => this.getBackgroundEvents(...args));
67
- this.dailyCheckIn();
68
- __classPrivateFieldGet(this, _CronjobController_instances, "m", _CronjobController_rescheduleBackgroundEvents).call(this, Object.values(this.state.events));
33
+ this.#timers = new Map();
34
+ this.messagingSystem.subscribe('SnapController:snapInstalled', this.#handleSnapInstalledEvent);
35
+ this.messagingSystem.subscribe('SnapController:snapUninstalled', this.#handleSnapUninstalledEvent);
36
+ this.messagingSystem.subscribe('SnapController:snapEnabled', this.#handleSnapEnabledEvent);
37
+ this.messagingSystem.subscribe('SnapController:snapDisabled', this.#handleSnapDisabledEvent);
38
+ this.messagingSystem.subscribe('SnapController:snapUpdated', this.#handleSnapUpdatedEvent);
39
+ this.messagingSystem.registerActionHandler(`${controllerName}:schedule`, (...args) => this.schedule(...args));
40
+ this.messagingSystem.registerActionHandler(`${controllerName}:cancel`, (...args) => this.cancel(...args));
41
+ this.messagingSystem.registerActionHandler(`${controllerName}:get`, (...args) => this.get(...args));
42
+ this.#start();
43
+ this.#clear();
44
+ this.#reschedule();
69
45
  }
70
46
  /**
71
- * Register cron jobs for a given snap by getting specification from a permission caveats.
72
- * Once registered, each job will be scheduled.
47
+ * Schedule a non-recurring background event.
73
48
  *
74
- * @param snapId - ID of a snap.
49
+ * @param event - The event to schedule.
50
+ * @returns The ID of the scheduled event.
75
51
  */
76
- register(snapId) {
77
- const jobs = __classPrivateFieldGet(this, _CronjobController_instances, "m", _CronjobController_getSnapJobs).call(this, snapId);
78
- jobs?.forEach((job) => __classPrivateFieldGet(this, _CronjobController_instances, "m", _CronjobController_schedule).call(this, job));
79
- }
80
- /**
81
- * Schedule a background event.
82
- *
83
- * @param backgroundEventWithoutId - Background event.
84
- * @returns An id representing the background event.
85
- */
86
- scheduleBackgroundEvent(backgroundEventWithoutId) {
87
- // Remove millisecond precision and convert to UTC.
88
- const scheduledAt = DateTime.fromJSDate(new Date())
89
- .toUTC()
90
- .startOf('second')
91
- .toISO({
92
- suppressMilliseconds: true,
52
+ schedule(event) {
53
+ return this.#add({
54
+ ...event,
55
+ recurring: false,
93
56
  });
94
- assert(scheduledAt);
95
- const event = {
96
- ...backgroundEventWithoutId,
97
- id: nanoid(),
98
- scheduledAt,
99
- };
100
- __classPrivateFieldGet(this, _CronjobController_instances, "m", _CronjobController_setUpBackgroundEvent).call(this, event);
101
- return event.id;
102
57
  }
103
58
  /**
104
- * Cancel a background event.
59
+ * Cancel an event.
105
60
  *
106
61
  * @param origin - The origin making the cancel call.
107
- * @param id - The id of the background event to cancel.
62
+ * @param id - The id of the event to cancel.
108
63
  * @throws If the event does not exist.
109
64
  */
110
- cancelBackgroundEvent(origin, id) {
65
+ cancel(origin, id) {
111
66
  assert(this.state.events[id], `A background event with the id of "${id}" does not exist.`);
112
67
  assert(this.state.events[id].snapId === origin, 'Only the origin that scheduled this event can cancel it.');
113
- const timer = __classPrivateFieldGet(this, _CronjobController_timers, "f").get(id);
114
- timer?.cancel();
115
- __classPrivateFieldGet(this, _CronjobController_timers, "f").delete(id);
116
- __classPrivateFieldGet(this, _CronjobController_snapIds, "f").delete(id);
117
- this.update((state) => {
118
- delete state.events[id];
119
- });
68
+ this.#cancel(id);
120
69
  }
121
70
  /**
122
71
  * Get a list of a Snap's background events.
@@ -124,251 +73,276 @@ export class CronjobController extends BaseController {
124
73
  * @param snapId - The id of the Snap to fetch background events for.
125
74
  * @returns An array of background events.
126
75
  */
127
- getBackgroundEvents(snapId) {
76
+ get(snapId) {
128
77
  return Object.values(this.state.events)
129
- .filter((snapEvent) => snapEvent.snapId === snapId)
78
+ .filter((snapEvent) => snapEvent.snapId === snapId && !snapEvent.recurring)
130
79
  .map((event) => ({
131
80
  ...event,
132
- // Truncate dates to remove milliseconds.
133
81
  date: toCensoredISO8601String(event.date),
82
+ scheduledAt: toCensoredISO8601String(event.scheduledAt),
134
83
  }));
135
84
  }
136
85
  /**
137
- * Unregister all jobs and background events related to the given snapId.
86
+ * Register cronjobs for a given Snap by getting specification from the
87
+ * permission caveats. Once registered, each job will be scheduled.
138
88
  *
139
- * @param snapId - ID of a snap.
140
- * @param skipEvents - Whether the unregistration process should skip scheduled background events.
89
+ * @param snapId - The snap ID to register jobs for.
141
90
  */
142
- unregister(snapId, skipEvents = false) {
143
- const jobs = [...__classPrivateFieldGet(this, _CronjobController_snapIds, "f").entries()].filter(([_, jobSnapId]) => jobSnapId === snapId);
144
- if (jobs.length) {
145
- const eventIds = [];
146
- jobs.forEach(([id]) => {
147
- const timer = __classPrivateFieldGet(this, _CronjobController_timers, "f").get(id);
148
- if (timer) {
149
- timer.cancel();
150
- __classPrivateFieldGet(this, _CronjobController_timers, "f").delete(id);
151
- __classPrivateFieldGet(this, _CronjobController_snapIds, "f").delete(id);
152
- if (!skipEvents && this.state.events[id]) {
153
- eventIds.push(id);
154
- }
155
- }
156
- });
157
- if (eventIds.length > 0) {
158
- this.update((state) => {
159
- eventIds.forEach((id) => {
160
- delete state.events[id];
161
- });
162
- });
163
- }
164
- }
91
+ register(snapId) {
92
+ const jobs = this.#getSnapCronjobs(snapId);
93
+ jobs?.forEach((job) => this.#add(job));
165
94
  }
166
95
  /**
167
- * Runs every 24 hours to check if new jobs need to be scheduled.
96
+ * Unregister all cronjobs and background events for a given Snap.
168
97
  *
169
- * This is necessary for longer running jobs that execute with more than 24 hours between them.
98
+ * @param snapId - ID of a snap.
170
99
  */
171
- dailyCheckIn() {
172
- const jobs = __classPrivateFieldGet(this, _CronjobController_instances, "m", _CronjobController_getAllJobs).call(this);
173
- for (const job of jobs) {
174
- const parsed = parseCronExpression(job.expression);
175
- const lastRun = this.state.jobs[job.id]?.lastRun;
176
- // If a job was supposed to run while we were shut down but wasn't we run it now
177
- if (lastRun !== undefined &&
178
- parsed.hasPrev() &&
179
- parsed.prev().getTime() > lastRun) {
180
- __classPrivateFieldGet(this, _CronjobController_instances, "m", _CronjobController_executeCronjob).call(this, job).catch((error) => {
181
- logError(error);
182
- });
100
+ unregister(snapId) {
101
+ for (const [id, event] of Object.entries(this.state.events)) {
102
+ if (event.snapId === snapId) {
103
+ this.#cancel(id);
183
104
  }
184
- // Try scheduling, will fail if an existing scheduled job is found
185
- __classPrivateFieldGet(this, _CronjobController_instances, "m", _CronjobController_schedule).call(this, job);
186
105
  }
187
- __classPrivateFieldSet(this, _CronjobController_dailyTimer, new Timer(DAILY_TIMEOUT), "f");
188
- __classPrivateFieldGet(this, _CronjobController_dailyTimer, "f").start(() => {
189
- this.dailyCheckIn();
190
- });
191
106
  }
192
107
  /**
193
108
  * Run controller teardown process and unsubscribe from Snap events.
194
109
  */
195
110
  destroy() {
196
111
  super.destroy();
197
- /* eslint-disable @typescript-eslint/unbound-method */
198
- this.messagingSystem.unsubscribe('SnapController:snapInstalled', this._handleSnapRegisterEvent);
199
- this.messagingSystem.unsubscribe('SnapController:snapUninstalled', this._handleSnapUnregisterEvent);
200
- this.messagingSystem.unsubscribe('SnapController:snapEnabled', this._handleSnapEnabledEvent);
201
- this.messagingSystem.unsubscribe('SnapController:snapDisabled', this._handleSnapDisabledEvent);
202
- this.messagingSystem.unsubscribe('SnapController:snapUpdated', this._handleEventSnapUpdated);
203
- /* eslint-enable @typescript-eslint/unbound-method */
204
- __classPrivateFieldGet(this, _CronjobController_snapIds, "f").forEach((snapId) => this.unregister(snapId));
112
+ this.messagingSystem.unsubscribe('SnapController:snapInstalled', this.#handleSnapInstalledEvent);
113
+ this.messagingSystem.unsubscribe('SnapController:snapUninstalled', this.#handleSnapUninstalledEvent);
114
+ this.messagingSystem.unsubscribe('SnapController:snapEnabled', this.#handleSnapEnabledEvent);
115
+ this.messagingSystem.unsubscribe('SnapController:snapDisabled', this.#handleSnapDisabledEvent);
116
+ this.messagingSystem.unsubscribe('SnapController:snapUpdated', this.#handleSnapUpdatedEvent);
117
+ // Cancel all timers and clear the map.
118
+ this.#timers.forEach((timer) => timer.cancel());
119
+ this.#timers.clear();
120
+ if (this.#dailyTimer.status === 'running') {
121
+ this.#dailyTimer.cancel();
122
+ }
205
123
  }
206
124
  /**
207
- * Handle events that should cause cronjobs to be registered.
208
- *
209
- * @param snap - Basic Snap information.
125
+ * Start the daily timer that will reschedule events every 24 hours.
210
126
  */
211
- // TODO: Either fix this lint violation or explain why it's necessary to
212
- // ignore.
213
- // eslint-disable-next-line no-restricted-syntax
214
- _handleSnapRegisterEvent(snap) {
215
- this.register(snap.id);
127
+ #start() {
128
+ this.#dailyTimer = new Timer(DAILY_TIMEOUT);
129
+ this.#dailyTimer.start(() => {
130
+ this.#reschedule();
131
+ this.#start();
132
+ });
216
133
  }
217
134
  /**
218
- * Handle events that could cause cronjobs to be registered
219
- * and for background events to be rescheduled.
135
+ * Add a cronjob or background event to the controller state and schedule it
136
+ * for execution.
220
137
  *
221
- * @param snap - Basic Snap information.
138
+ * @param event - The event to schedule.
139
+ * @returns The ID of the scheduled event.
222
140
  */
223
- // TODO: Either fix this lint violation or explain why it's necessary to
224
- // ignore.
225
- // eslint-disable-next-line no-restricted-syntax
226
- _handleSnapEnabledEvent(snap) {
227
- const events = this.getBackgroundEvents(snap.id);
228
- __classPrivateFieldGet(this, _CronjobController_instances, "m", _CronjobController_rescheduleBackgroundEvents).call(this, events);
229
- this.register(snap.id);
141
+ #add(event) {
142
+ const id = event.id ?? nanoid();
143
+ const internalEvent = {
144
+ ...event,
145
+ id,
146
+ date: getExecutionDate(event.schedule),
147
+ scheduledAt: new Date().toISOString(),
148
+ };
149
+ this.update((state) => {
150
+ state.events[internalEvent.id] = castDraft(internalEvent);
151
+ });
152
+ this.#schedule(internalEvent);
153
+ return id;
230
154
  }
231
155
  /**
232
- * Handle events that should cause cronjobs and background events to be unregistered.
156
+ * Get the next execution date for a given event and start a timer for it.
233
157
  *
234
- * @param snap - Basic Snap information.
158
+ * @param event - The event to schedule.
235
159
  */
236
- // TODO: Either fix this lint violation or explain why it's necessary to
237
- // ignore.
238
- // eslint-disable-next-line no-restricted-syntax
239
- _handleSnapUnregisterEvent(snap) {
240
- this.unregister(snap.id);
160
+ #schedule(event) {
161
+ const date = getExecutionDate(event.schedule);
162
+ this.update((state) => {
163
+ state.events[event.id].date = date;
164
+ });
165
+ this.#startTimer({
166
+ ...event,
167
+ date,
168
+ });
241
169
  }
242
170
  /**
243
- * Handle events that should cause cronjobs and background events to be unregistered.
171
+ * Set up and start a timer for the given event.
244
172
  *
245
- * @param snap - Basic Snap information.
173
+ * @param event - The event to schedule.
174
+ * @throws If the event is scheduled in the past.
246
175
  */
247
- // TODO: Either fix this lint violation or explain why it's necessary to
248
- // ignore.
249
- // eslint-disable-next-line no-restricted-syntax
250
- _handleSnapDisabledEvent(snap) {
251
- this.unregister(snap.id, true);
176
+ #startTimer(event) {
177
+ const ms = DateTime.fromISO(event.date, { setZone: true }).toMillis() - Date.now();
178
+ // We don't schedule this job yet as it is too far in the future.
179
+ if (ms > DAILY_TIMEOUT) {
180
+ return;
181
+ }
182
+ const timer = new Timer(ms);
183
+ timer.start(() => {
184
+ this.#execute(event);
185
+ });
186
+ this.#timers.set(event.id, timer);
252
187
  }
253
188
  /**
254
- * Handle cron jobs on 'snapUpdated' event.
189
+ * Execute a background event. This method is called when the event's timer
190
+ * expires.
255
191
  *
256
- * @param snap - Basic Snap information.
192
+ * If the event is not recurring, it will be removed from the state after
193
+ * execution. If it is recurring, it will be rescheduled.
194
+ *
195
+ * @param event - The event to execute.
257
196
  */
258
- // TODO: Either fix this lint violation or explain why it's necessary to
259
- // ignore.
260
- // eslint-disable-next-line no-restricted-syntax
261
- _handleEventSnapUpdated(snap) {
262
- this.unregister(snap.id);
263
- this.register(snap.id);
264
- }
265
- }
266
- _CronjobController_dailyTimer = new WeakMap(), _CronjobController_timers = new WeakMap(), _CronjobController_snapIds = new WeakMap(), _CronjobController_instances = new WeakSet(), _CronjobController_getAllJobs = function _CronjobController_getAllJobs() {
267
- const snaps = this.messagingSystem.call('SnapController:getAll');
268
- const filteredSnaps = getRunnableSnaps(snaps);
269
- const jobs = filteredSnaps.map((snap) => __classPrivateFieldGet(this, _CronjobController_instances, "m", _CronjobController_getSnapJobs).call(this, snap.id));
270
- return jobs.flat().filter((job) => job !== undefined);
271
- }, _CronjobController_getSnapJobs = function _CronjobController_getSnapJobs(snapId) {
272
- const permissions = this.messagingSystem.call('PermissionController:getPermissions', snapId);
273
- const permission = permissions?.[SnapEndowments.Cronjob];
274
- const definitions = getCronjobCaveatJobs(permission);
275
- return definitions?.map((definition, idx) => {
276
- return { ...definition, id: `${snapId}-${idx}`, snapId };
277
- });
278
- }, _CronjobController_schedule = function _CronjobController_schedule(job) {
279
- if (__classPrivateFieldGet(this, _CronjobController_timers, "f").has(job.id)) {
280
- return;
281
- }
282
- const parsed = parseCronExpression(job.expression);
283
- const next = parsed.next();
284
- const now = new Date();
285
- const ms = next.getTime() - now.getTime();
286
- // Don't schedule this job yet as it is too far in the future
287
- if (ms > DAILY_TIMEOUT) {
288
- return;
289
- }
290
- const timer = new Timer(ms);
291
- timer.start(() => {
292
- __classPrivateFieldGet(this, _CronjobController_instances, "m", _CronjobController_executeCronjob).call(this, job).catch((error) => {
293
- // TODO: Decide how to handle errors.
294
- logError(error);
295
- });
296
- __classPrivateFieldGet(this, _CronjobController_timers, "f").delete(job.id);
297
- __classPrivateFieldGet(this, _CronjobController_instances, "m", _CronjobController_schedule).call(this, job);
298
- });
299
- if (!this.state.jobs[job.id]?.lastRun) {
300
- __classPrivateFieldGet(this, _CronjobController_instances, "m", _CronjobController_updateJobLastRunState).call(this, job.id, 0); // 0 for init, never ran actually
301
- }
302
- __classPrivateFieldGet(this, _CronjobController_timers, "f").set(job.id, timer);
303
- __classPrivateFieldGet(this, _CronjobController_snapIds, "f").set(job.id, job.snapId);
304
- }, _CronjobController_executeCronjob =
305
- /**
306
- * Execute job.
307
- *
308
- * @param job - Cronjob specification.
309
- */
310
- async function _CronjobController_executeCronjob(job) {
311
- __classPrivateFieldGet(this, _CronjobController_instances, "m", _CronjobController_updateJobLastRunState).call(this, job.id, Date.now());
312
- await this.messagingSystem.call('SnapController:handleRequest', {
313
- snapId: job.snapId,
314
- origin: 'metamask',
315
- handler: HandlerType.OnCronjob,
316
- request: job.request,
317
- });
318
- }, _CronjobController_setUpBackgroundEvent = function _CronjobController_setUpBackgroundEvent(event) {
319
- const date = new Date(event.date);
320
- const now = new Date();
321
- const ms = date.getTime() - now.getTime();
322
- if (ms <= 0) {
323
- throw new Error('Cannot schedule an event in the past.');
324
- }
325
- // The event may already be in state when we get here.
326
- if (!hasProperty(this.state.events, event.id)) {
327
- this.update((state) => {
328
- state.events[event.id] = castDraft(event);
329
- });
330
- }
331
- const timer = new Timer(ms);
332
- timer.start(() => {
197
+ #execute(event) {
333
198
  this.messagingSystem
334
199
  .call('SnapController:handleRequest', {
335
200
  snapId: event.snapId,
336
- origin: 'metamask',
201
+ origin: METAMASK_ORIGIN,
337
202
  handler: HandlerType.OnCronjob,
338
203
  request: event.request,
339
204
  })
340
205
  .catch((error) => {
341
- logError(error);
206
+ logError(`An error occurred while executing an event for Snap "${event.snapId}":`, error);
342
207
  });
343
- __classPrivateFieldGet(this, _CronjobController_timers, "f").delete(event.id);
344
- __classPrivateFieldGet(this, _CronjobController_snapIds, "f").delete(event.id);
345
- this.update((state) => {
346
- delete state.events[event.id];
347
- });
348
- });
349
- __classPrivateFieldGet(this, _CronjobController_timers, "f").set(event.id, timer);
350
- __classPrivateFieldGet(this, _CronjobController_snapIds, "f").set(event.id, event.snapId);
351
- }, _CronjobController_updateJobLastRunState = function _CronjobController_updateJobLastRunState(jobId, lastRun) {
352
- this.update((state) => {
353
- state.jobs[jobId] = {
354
- lastRun,
355
- };
356
- });
357
- }, _CronjobController_rescheduleBackgroundEvents = function _CronjobController_rescheduleBackgroundEvents(backgroundEvents) {
358
- for (const snapEvent of backgroundEvents) {
359
- const { date } = snapEvent;
360
- const now = new Date();
361
- const then = new Date(date);
362
- if (then.getTime() < now.getTime()) {
363
- // Remove expired events from state
208
+ this.#timers.delete(event.id);
209
+ // Non-recurring events are removed from the state after execution, and
210
+ // recurring events are rescheduled.
211
+ if (!event.recurring) {
364
212
  this.update((state) => {
365
- delete state.events[snapEvent.id];
213
+ delete state.events[event.id];
366
214
  });
367
- logWarning(`Background event with id "${snapEvent.id}" not scheduled as its date has expired.`);
215
+ return;
216
+ }
217
+ this.#schedule(event);
218
+ }
219
+ /**
220
+ * Cancel a background event by its ID. Unlike {@link cancel}, this method
221
+ * does not check the origin of the event, so it can be used internally.
222
+ *
223
+ * @param id - The ID of the background event to cancel.
224
+ */
225
+ #cancel(id) {
226
+ const timer = this.#timers.get(id);
227
+ timer?.cancel();
228
+ this.#timers.delete(id);
229
+ this.update((state) => {
230
+ delete state.events[id];
231
+ });
232
+ }
233
+ /**
234
+ * Retrieve all cronjob specifications for a Snap.
235
+ *
236
+ * @param snapId - ID of a Snap.
237
+ * @returns Array of cronjob specifications.
238
+ */
239
+ #getSnapCronjobs(snapId) {
240
+ const permissions = this.messagingSystem.call('PermissionController:getPermissions', snapId);
241
+ const permission = permissions?.[SnapEndowments.Cronjob];
242
+ const definitions = getCronjobCaveatJobs(permission);
243
+ if (!definitions) {
244
+ return [];
245
+ }
246
+ return definitions.map((definition, idx) => {
247
+ return {
248
+ snapId,
249
+ id: `cronjob-${snapId}-${idx}`,
250
+ request: definition.request,
251
+ schedule: getCronjobSpecificationSchedule(definition),
252
+ recurring: true,
253
+ };
254
+ });
255
+ }
256
+ /**
257
+ * Handle events that should cause cron jobs to be registered.
258
+ *
259
+ * @param snap - Basic Snap information.
260
+ */
261
+ #handleSnapInstalledEvent = (snap) => {
262
+ this.register(snap.id);
263
+ };
264
+ /**
265
+ * Handle the Snap enabled event. This checks if the Snap has any cronjobs or
266
+ * background events that need to be rescheduled.
267
+ *
268
+ * @param snap - Basic Snap information.
269
+ */
270
+ #handleSnapEnabledEvent = (snap) => {
271
+ const events = this.get(snap.id);
272
+ this.#reschedule(events);
273
+ this.register(snap.id);
274
+ };
275
+ /**
276
+ * Handle events that should cause cronjobs and background events to be
277
+ * unregistered.
278
+ *
279
+ * @param snap - Basic Snap information.
280
+ */
281
+ #handleSnapUninstalledEvent = (snap) => {
282
+ this.unregister(snap.id);
283
+ };
284
+ /**
285
+ * Handle events that should cause cronjobs and background events to be
286
+ * unregistered.
287
+ *
288
+ * @param snap - Basic Snap information.
289
+ */
290
+ #handleSnapDisabledEvent = (snap) => {
291
+ this.unregister(snap.id);
292
+ };
293
+ /**
294
+ * Handle cron jobs on 'snapUpdated' event.
295
+ *
296
+ * @param snap - Basic Snap information.
297
+ */
298
+ #handleSnapUpdatedEvent = (snap) => {
299
+ this.unregister(snap.id);
300
+ this.register(snap.id);
301
+ };
302
+ /**
303
+ * Reschedule events that are yet to be executed. This should be called on
304
+ * controller initialization and once every 24 hours to ensure that
305
+ * background events are scheduled correctly.
306
+ *
307
+ * @param events - An array of events to reschedule. Defaults to all events in
308
+ * the controller state.
309
+ */
310
+ #reschedule(events = Object.values(this.state.events)) {
311
+ const now = Date.now();
312
+ for (const event of events) {
313
+ if (this.#timers.has(event.id)) {
314
+ // If the timer for this event already exists, we don't need to
315
+ // reschedule it.
316
+ continue;
317
+ }
318
+ const eventDate = DateTime.fromISO(event.date, {
319
+ setZone: true,
320
+ })
321
+ .toUTC()
322
+ .toMillis();
323
+ // If the event is recurring and the date is in the past, execute it
324
+ // immediately.
325
+ if (event.recurring && eventDate <= now) {
326
+ this.#execute(event);
327
+ }
328
+ this.#schedule(event);
368
329
  }
369
- else {
370
- __classPrivateFieldGet(this, _CronjobController_instances, "m", _CronjobController_setUpBackgroundEvent).call(this, snapEvent);
330
+ }
331
+ /**
332
+ * Clear non-recurring events that are past their scheduled time.
333
+ */
334
+ #clear() {
335
+ const now = Date.now();
336
+ for (const event of Object.values(this.state.events)) {
337
+ const eventDate = DateTime.fromISO(event.date, {
338
+ setZone: true,
339
+ })
340
+ .toUTC()
341
+ .toMillis();
342
+ if (!event.recurring && eventDate < now) {
343
+ this.#cancel(event.id);
344
+ }
371
345
  }
372
346
  }
373
- };
347
+ }
374
348
  //# sourceMappingURL=CronjobController.mjs.map