diff --git a/libs/permissions/permissionLogic/src/lib/actorIds.ts b/libs/permissions/permissionLogic/src/lib/actorIds.ts new file mode 100644 index 0000000..eb2d916 --- /dev/null +++ b/libs/permissions/permissionLogic/src/lib/actorIds.ts @@ -0,0 +1,9 @@ +export const ActorSystemIds = { + permissionMonitoring: 'permissionMonitoringMachineId', + permissionReporting: 'permissionReportingMachineId', + permissionCheckerAndRequester: 'permissionCheckerAndRequesterMachineId', + + lifecycleReporting: 'lifecycleReportingMachineId', + + features: 'featuresMachineId', +} as const; diff --git a/libs/permissions/permissionLogic/src/lib/inspectorExploration/inspector.spec.ts b/libs/permissions/permissionLogic/src/lib/inspectorExploration/inspector.spec.ts new file mode 100644 index 0000000..4de3cc9 --- /dev/null +++ b/libs/permissions/permissionLogic/src/lib/inspectorExploration/inspector.spec.ts @@ -0,0 +1,113 @@ +const prettyMuchForever = Math.pow(2, 31) - 1; + +export type SimpleInspectorOptions = { + onLiveInspectActive?: (url: string) => Promise; +}; + +export function createSimpleInspector(options: SimpleInspectorOptions = {}) { + const { onLiveInspectActive } = options; + const liveInspectUrl = 'https://example.com/inspect/session123'; + + // Simulate the WebSocket onopen event with a promise that resolves after 500ms + const socketOpenPromise = new Promise((resolve) => { + setTimeout(() => { + console.log('WebSocket opened'); + resolve(); + }, 500); + }); + + // Return an object with a method to wait for the live inspect session to be active + return { + waitForLiveInspectActive: async () => { + await socketOpenPromise; + if (onLiveInspectActive) { + await onLiveInspectActive(liveInspectUrl); + } + }, + }; +} + +// describe('createSkyInspector', () => { +// it( +// /* ⚠️failing attempt to debug with stately sky*/ 'should wait for the live inspect session to be active', +// async () => { +// const mockCallback = jest.fn(); +// +// const { waitForLiveInspectActive, inspector } = createSkyInspector({ +// onLiveInspectActive: async (url) => { +// await new Promise((resolve) => setTimeout(resolve, 1000)); +// mockCallback(url); +// }, +// }); +// +// // Call the waitForLiveInspectActive function without awaiting its completion +// const waitPromise = waitForLiveInspectActive(); +// +// // Assert that the inspector object is created +// expect(inspector).toBeDefined(); +// +// // Wait for a short time (less than the WebSocket open delay and callback delay) +// await new Promise((resolve) => setTimeout(resolve, 300)); +// +// // Assert that the callback has not been called yet +// expect(mockCallback).not.toHaveBeenCalled(); +// +// // Wait for the waitForLiveInspectActive promise to resolve +// /* ✅This is properly being awaited*/ await waitPromise; +// +// // Assert that the callback has been called with the correct URL +// // expect(mockCallback).toHaveBeenCalledWith( +// // 'https://stately.ai/inspect/session123' +// // ); +// +// const countingActor = createActor(countingMachineThatNeedsPermissionAt3, { +// inspect: inspector.inspect, +// }).start(); +// +// /* 🤔 If I set a brekapoint here, then the inspector won't "connect" until the promise at the bottom*/ countingActor.send( +// { type: 'count.inc' } +// ); +// countingActor.send({ type: 'count.inc' }); +// countingActor.send({ type: 'count.inc' }); +// countingActor.send({ type: 'count.inc' }); +// expect(countingActor.getSnapshot().context.count).toBe(3); +// expect(countingActor.getSnapshot().value).toStrictEqual({ +// counting: 'disabled', +// handlingPermissions: 'active', +// }); +// +// await new Promise((resolve) => setTimeout(resolve, prettyMuchForever)); +// }, +// prettyMuchForever +// ); +// }); + +describe('createSimpleInspector', () => { + it('should wait for the live inspect session to be active', async () => { + const mockCallback = jest.fn(); + + const inspector = createSimpleInspector({ + onLiveInspectActive: async (url) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + mockCallback(url); + }, + }); + + // Call the waitForLiveInspectActive method without awaiting its completion + const waitPromise = inspector.waitForLiveInspectActive(); + + // Wait for a short time (less than the WebSocket open delay and callback delay) + await new Promise((resolve) => setTimeout(resolve, 300)); + + // Assert that the callback has not been called yet + expect(mockCallback).not.toHaveBeenCalled(); + + // Wait for the waitForLiveInspectActive promise to resolve + await waitPromise; + + // Assert that the callback has been called with the correct URL + expect(mockCallback).toHaveBeenCalledWith( + 'https://example.com/inspect/session123' + ); + }); +}); diff --git a/libs/permissions/permissionLogic/src/lib/permission-logic.spec.ts b/libs/permissions/permissionLogic/src/lib/permission-logic.spec.ts index f4c0067..109cf49 100644 --- a/libs/permissions/permissionLogic/src/lib/permission-logic.spec.ts +++ b/libs/permissions/permissionLogic/src/lib/permission-logic.spec.ts @@ -1,5 +1,5 @@ // permissionMonitoringMachine.test.ts -const prettyMuchForever = Math.pow(2, 31) - 1; +import { ActorSystemIds } from './actorIds'; import { AnyActorRef, @@ -26,12 +26,6 @@ import { permissionMonitoringMachine, } from './permissionMonitor.machine'; -const ActorSystemIds = { - permissionMonitoring: 'permissionMonitoringMachineId', - permissionReporting: 'permissionReportingMachineId', - permissionCheckerAndRequester: 'permissionCheckerAndRequesterMachineId', -} as const; - const countingMachineThatNeedsPermissionAt3 = setup({ types: { context: {} as { count: number; permissionStatus: PermissionStatus }, @@ -106,131 +100,13 @@ const countingMachineThatNeedsPermissionAt3 = setup({ }, }, }); -export type SimpleInspectorOptions = { - onLiveInspectActive?: (url: string) => Promise; -}; - -export function createSimpleInspector(options: SimpleInspectorOptions = {}) { - const { onLiveInspectActive } = options; - const liveInspectUrl = 'https://example.com/inspect/session123'; - - // Simulate the WebSocket onopen event with a promise that resolves after 500ms - const socketOpenPromise = new Promise((resolve) => { - setTimeout(() => { - console.log('WebSocket opened'); - resolve(); - }, 500); - }); - - // Return an object with a method to wait for the live inspect session to be active - return { - waitForLiveInspectActive: async () => { - await socketOpenPromise; - if (onLiveInspectActive) { - await onLiveInspectActive(liveInspectUrl); - } - }, - }; -} - -// describe('createSkyInspector', () => { -// it( -// /* ⚠️failing attempt to debug with stately sky*/ 'should wait for the live inspect session to be active', -// async () => { -// const mockCallback = jest.fn(); -// -// const { waitForLiveInspectActive, inspector } = createSkyInspector({ -// onLiveInspectActive: async (url) => { -// await new Promise((resolve) => setTimeout(resolve, 1000)); -// mockCallback(url); -// }, -// }); -// -// // Call the waitForLiveInspectActive function without awaiting its completion -// const waitPromise = waitForLiveInspectActive(); -// -// // Assert that the inspector object is created -// expect(inspector).toBeDefined(); -// -// // Wait for a short time (less than the WebSocket open delay and callback delay) -// await new Promise((resolve) => setTimeout(resolve, 300)); -// -// // Assert that the callback has not been called yet -// expect(mockCallback).not.toHaveBeenCalled(); -// -// // Wait for the waitForLiveInspectActive promise to resolve -// /* ✅This is properly being awaited*/ await waitPromise; -// -// // Assert that the callback has been called with the correct URL -// // expect(mockCallback).toHaveBeenCalledWith( -// // 'https://stately.ai/inspect/session123' -// // ); -// -// const countingActor = createActor(countingMachineThatNeedsPermissionAt3, { -// inspect: inspector.inspect, -// }).start(); -// -// /* 🤔 If I set a brekapoint here, then the inspector won't "connect" until the promise at the bottom*/ countingActor.send( -// { type: 'count.inc' } -// ); -// countingActor.send({ type: 'count.inc' }); -// countingActor.send({ type: 'count.inc' }); -// countingActor.send({ type: 'count.inc' }); -// expect(countingActor.getSnapshot().context.count).toBe(3); -// expect(countingActor.getSnapshot().value).toStrictEqual({ -// counting: 'disabled', -// handlingPermissions: 'active', -// }); -// -// await new Promise((resolve) => setTimeout(resolve, prettyMuchForever)); -// }, -// prettyMuchForever -// ); -// }); - -describe('createSimpleInspector', () => { - it('should wait for the live inspect session to be active', async () => { - const mockCallback = jest.fn(); - - const inspector = createSimpleInspector({ - onLiveInspectActive: async (url) => { - await new Promise((resolve) => setTimeout(resolve, 1000)); - mockCallback(url); - }, - }); - - // Call the waitForLiveInspectActive method without awaiting its completion - const waitPromise = inspector.waitForLiveInspectActive(); - - // Wait for a short time (less than the WebSocket open delay and callback delay) - await new Promise((resolve) => setTimeout(resolve, 300)); - - // Assert that the callback has not been called yet - expect(mockCallback).not.toHaveBeenCalled(); - - // Wait for the waitForLiveInspectActive promise to resolve - await waitPromise; - - // Assert that the callback has been called with the correct URL - expect(mockCallback).toHaveBeenCalledWith( - 'https://example.com/inspect/session123' - ); - }); -}); describe('Counting Machine That Needs Permission At 3', () => { it('should not increment count beyond 3, but rather ask permission', async () => { - // const inspector = await createSkyInspector({ - // onerror: (err) => console.log(err), - // onLiveInspectActive: async (url) => { - // console.log('Live inspect session is active!'); - // console.log('URL:', url); - // console.log('Async operations completed!'); - // }, - // }); - const countingActor = createActor(countingMachineThatNeedsPermissionAt3, { - // inspect: inspector.inspect, - }).start(); + const countingActor = createActor( + countingMachineThatNeedsPermissionAt3, + {} + ).start(); countingActor.send({ type: 'count.inc' }); countingActor.send({ type: 'count.inc' }); @@ -306,9 +182,7 @@ describe('Permission Requester and Checker Machine', () => { const parentMachine = setup({ types: {} as { events: PermissionMonitoringMachineEvents; - children: { - [ActorSystemIds.permissionCheckerAndRequester]: 'permissionCheckerAndRequesterMachine'; - }; + children: {}; }, actors: { @@ -390,7 +264,12 @@ describe('Permission Requester and Checker Machine', () => { }; const parentMachine = setup({ - types: {} as { events: PermissionMonitoringMachineEvents }, + types: {} as { + events: PermissionMonitoringMachineEvents; + children: { + [ActorSystemIds.permissionCheckerAndRequester]: 'permissionCheckerAndRequesterMachine'; + }; + }, actors: { permissionCheckerAndRequesterMachine, }, @@ -401,7 +280,7 @@ describe('Permission Requester and Checker Machine', () => { }, triggerPermissionRequest: { actions: [ - sendTo('someFooMachine', { + sendTo(ActorSystemIds.permissionCheckerAndRequester, { type: 'triggerPermissionRequest', permission: Permissions.bluetooth, }), @@ -409,7 +288,7 @@ describe('Permission Requester and Checker Machine', () => { }, }, invoke: { - id: 'someFooMachine', + id: ActorSystemIds.permissionCheckerAndRequester, src: 'permissionCheckerAndRequesterMachine', input: ({ self }) => ({ parent: self }), }, @@ -423,7 +302,10 @@ describe('Permission Requester and Checker Machine', () => { await waitFor( actorRef, - (state) => state.children.someFooMachine?.getSnapshot().value === 'idle' + (state) => + state.children[ + ActorSystemIds.permissionCheckerAndRequester + ]!.getSnapshot().value === 'idle' ); expect(result).not.toBeNull(); @@ -689,8 +571,8 @@ describe('Permission Monitoring Machine', () => { await waitFor(actorRef, (state) => { return ( - // @ts-expect-error - state.children.someFooMachine?.getSnapshot().value === 'idle' + state.children.permissionCheckerAndRequesterMachineId.getSnapshot() + .value === 'idle' ); }); @@ -705,13 +587,16 @@ describe('Permission Monitoring Machine', () => { }); expect( - // @ts-expect-error - actorRef.getSnapshot().children.someFooMachine?.getSnapshot().value + actorRef + .getSnapshot() + .children.permissionCheckerAndRequesterMachineId.getSnapshot().value ).toBe('requestingPermission'); await waitFor(actorRef, (state) => { - // @ts-expect-error - return state.children.someFooMachine?.getSnapshot().value === 'idle'; + return ( + state.children.permissionCheckerAndRequesterMachineId.getSnapshot() + .value === 'idle' + ); }); expect(actorRef.getSnapshot().context.permissionsStatuses).toStrictEqual({ diff --git a/libs/permissions/permissionLogic/src/lib/permissionMonitor.machine.ts b/libs/permissions/permissionLogic/src/lib/permissionMonitor.machine.ts index 51ba66d..60ada7a 100644 --- a/libs/permissions/permissionLogic/src/lib/permissionMonitor.machine.ts +++ b/libs/permissions/permissionLogic/src/lib/permissionMonitor.machine.ts @@ -5,9 +5,9 @@ import { PermissionStatusMapType, } from './permission.types'; import { + AnyActorRef, assertEvent, assign, - createMachine, enqueueActions, log, raise, @@ -18,6 +18,7 @@ import { stubApplicationLifecycleReportingActorLogic } from './lifecycle/lifecyc import { InitialPermissionStatusMap } from './permission.fixtures'; import { PermissionSubscriberMap } from './permission-logic.spec'; import { permissionCheckerAndRequesterMachine } from './permissionCheckAndRequestMachine'; +import { ActorSystemIds } from './actorIds'; export const EmptyPermissionSubscriberMap: PermissionSubscriberMap = Object.values(Permissions).reduce( @@ -35,6 +36,11 @@ export const permissionMonitoringMachine = setup({ types: {} as { events: PermissionMonitoringMachineEvents; context: PermissionsMonitoringMachineContext; + children: { + [ActorSystemIds.permissionCheckerAndRequester]: 'permissionCheckerAndRequesterMachine'; + [ActorSystemIds.lifecycleReporting]: 'applicationLifecycleReportingMachine'; + [ActorSystemIds.features]: 'features'; + }; }, actors: { applicationLifecycleReportingMachine: @@ -52,16 +58,16 @@ export const permissionMonitoringMachine = setup({ }), broadcastPermissionsToListeners: enqueueActions( ({ context, event, enqueue }) => { - console.log(event); Object.keys(context.permissionSubscribers).forEach((permission) => { - context.permissionSubscribers[permission].forEach((actorRef) => { - enqueue.sendTo(actorRef, { - type: 'permissionStatusChanged', - // @ts-expect-error TODO type these - permission, - status: context.permissionsStatuses[permission], - }); - }); + context.permissionSubscribers[permission].forEach( + (actorRef: AnyActorRef) => { + enqueue.sendTo(actorRef, { + type: 'permissionStatusChanged', + permission, + status: context.permissionsStatuses[permission], + }); + } + ); }); } ), @@ -75,25 +81,34 @@ export const permissionMonitoringMachine = setup({ }, }), raisePermissionCheck: raise({ type: 'triggerPermissionCheck' }), - sendPermissionCheck: sendTo('someFooMachine', { + sendPermissionCheck: sendTo(ActorSystemIds.permissionCheckerAndRequester, { type: 'triggerPermissionCheck', }), - sendPermissionRequest: sendTo('someFooMachine', ({ context, event }) => { - assertEvent(event, 'triggerPermissionRequest'); + sendPermissionRequest: sendTo( + ActorSystemIds.permissionCheckerAndRequester, + ({ context, event }) => { + assertEvent(event, 'triggerPermissionRequest'); - return { - type: 'triggerPermissionRequest', - permission: event.permission, - }; - }), + return { + type: 'triggerPermissionRequest', + permission: event.permission, + }; + } + ), }, }).createMachine({ /** @xstate-layout N4IgpgJg5mDOIC5QCMCWUDSBDAFgVwDssA6LABzIBtUBjLAF1QHsCAZVAMzBoE8bKwAYnJVaDZgQBiTAE5goMpoQiQA2gAYAuolBkmsVIxY6QAD0QBaABwBmdcQCcANicAWKwFY7D165tWAGhAeSwBGf2JQgHYbf3U7J38PACYogF80oLRMXEISEWo6IzZObj4BYQpC8RYAISwaAGsFJQIVCA1tJBA9A2KTcwQLVxjHZISoqz9bcI8gkKHwq0iYmydk9Sso9VdQq2SMrPRsfCJiMjAZAFtUWAMWWGFKSgAFS5u7iVgAYRxuRrUWhMvUMEgGiBsrmSxB2ySsVjcoS8O3Uc2CYSs9lCiSi218DiiHnU6gOmRA2ROeXO71u9wIj3oMnQMBkb2utIkv3+nWB+lBxm6gwsyVCjiRoQcW3iNiivj88zCDhsxBGoXUUQlMQRDg8VkO5OOuTOF3ZnwegkZzMubI+dIASmAAI54OD0HndEH9QWIZLQ4lOKwEgOBqa+tELSHQmW48bhX1rJz6ilGkgm21fQRpjksB3O13fJhXKhgeiArq6Ple0CDGzJVwq2sI6KeDzrcMQjz1zzYkVOGJKvsZMkEJgqeDdZOnLC8vpg71DBzJBwq1K+qFOTa2QLooY2DzLtwIpdwgPJAOJsmTqkFMTFdhcXj8MAz-kEcFDeGiqFRNdnzf+BUhlXYhaw8H8JR2WJXFRJNDSnUgqlvCR7zKJ8ENEIoJAASVgLCpFkeRFGUF8qzMSxbGVb9fw3eEAJ3CwPC-KIlQcJEbFCeN4lgnJ4JvTCWBQx8BHQ6pijw+omhaYiPUrOdq0sVwnGXH8NWJBwHGJCVXEA4VlnUNwnC8RcYy3C8jh4qkszNekSLksiEH2GF-E8fZ3GxSNAJlYhWysCDwkhXZ9yHNIgA */ - id: 'bigKahuna', + id: ActorSystemIds.permissionMonitoring, type: 'parallel', // TODO: this should live at the top level of the application, not here - invoke: [{ src: 'features' }], + invoke: [ + { + src: 'features', + systemId: ActorSystemIds.features, + id: ActorSystemIds.features, + }, + ], context: { permissionsStatuses: InitialPermissionStatusMap, @@ -183,7 +198,8 @@ export const permissionMonitoringMachine = setup({ }, }, invoke: { - id: 'someFooMachine', + id: ActorSystemIds.permissionCheckerAndRequester, + systemId: ActorSystemIds.permissionCheckerAndRequester, src: 'permissionCheckerAndRequesterMachine', input: ({ self }) => ({ parent: self }), },