@fanfare-io/fanfare-sdk-core 0.2.0 → 0.4.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 (53) hide show
  1. package/dist/appointments/appointment.driver.d.ts +44 -0
  2. package/dist/appointments/appointment.driver.js +1 -0
  3. package/dist/appointments/appointment.sequence.d.ts +194 -0
  4. package/dist/auctions/auction.driver.d.ts +46 -0
  5. package/dist/auctions/auction.driver.js +1 -0
  6. package/dist/auctions/auction.module.js +1 -1
  7. package/dist/auctions/auction.sequence.d.ts +263 -0
  8. package/dist/core/client.js +1 -1
  9. package/dist/draws/draw.driver.d.ts +45 -0
  10. package/dist/draws/draw.driver.js +1 -0
  11. package/dist/draws/draw.module.js +1 -1
  12. package/dist/draws/draw.sequence.d.ts +249 -0
  13. package/dist/experiences/distribution-monitor.types.d.ts +92 -2
  14. package/dist/experiences/experience.module.js +1 -1
  15. package/dist/experiences/journey-view.d.ts +8 -10
  16. package/dist/experiences/journey-view.js +1 -1
  17. package/dist/experiences/journey.d.ts +45 -20
  18. package/dist/experiences/journey.js +1 -1
  19. package/dist/experiences/journey.machine.d.ts +3 -20
  20. package/dist/experiences/journey.machine.js +1 -1
  21. package/dist/experiences/journey.types.d.ts +422 -217
  22. package/dist/experiences/public.d.ts +1 -1
  23. package/dist/experiences/sequence-driver.d.ts +93 -0
  24. package/dist/experiences/types.d.ts +185 -23
  25. package/dist/internals.d.ts +1 -1
  26. package/dist/internals.js +1 -1
  27. package/dist/queues/queue.driver.d.ts +45 -0
  28. package/dist/queues/queue.driver.js +1 -0
  29. package/dist/queues/queue.module.js +1 -1
  30. package/dist/queues/queue.sequence.d.ts +222 -0
  31. package/dist/state/capability-token-registry.d.ts +1 -0
  32. package/dist/state/capability-token-registry.js +1 -1
  33. package/dist/state/store.d.ts +5 -1
  34. package/dist/state/store.js +1 -1
  35. package/dist/storefront.d.ts +14 -24
  36. package/dist/storefront.js +1 -1
  37. package/dist/test-utils/harness-scenarios.d.ts +8 -1
  38. package/dist/test-utils/harness-scenarios.js +1 -1
  39. package/dist/test-utils/mock-journey.d.ts +15 -4
  40. package/dist/test-utils/mock-journey.js +1 -1
  41. package/dist/test-utils/mock-sdk.js +1 -1
  42. package/dist/test-utils/verification-scenarios.d.ts +9 -1
  43. package/dist/test-utils/verification-scenarios.js +1 -1
  44. package/dist/timed-releases/timed-release.driver.d.ts +45 -0
  45. package/dist/timed-releases/timed-release.driver.js +1 -0
  46. package/dist/timed-releases/timed-release.module.d.ts +6 -1
  47. package/dist/timed-releases/timed-release.module.js +1 -1
  48. package/dist/timed-releases/timed-release.sequence.d.ts +227 -0
  49. package/dist/timed-releases/types.d.ts +16 -1
  50. package/dist/types/distribution-type.d.ts +1 -1
  51. package/dist/version.d.ts +1 -1
  52. package/dist/version.js +1 -1
  53. package/package.json +3 -3
@@ -1,54 +1,144 @@
1
1
  import { SequenceOutcome } from '@fanfare-io/fanfare-sdk-contracts/consumer-me';
2
- export type MonitorUpdate = {
2
+ /**
3
+ * Terminal update emitted by a mechanism monitor.
4
+ *
5
+ * Monitors report only cross-cutting lifecycle changes that the journey shell
6
+ * must commit: a newly issued admission grant or a server-observed terminal
7
+ * outcome. Display-only values are written through the mechanism display atom.
8
+ */
9
+ export type MonitorUpdate =
10
+ /** The server issued an admission credential for this consumer. */
11
+ {
3
12
  type: "granted";
4
13
  token: string;
5
14
  expiresAt?: number;
6
- } | {
15
+ }
16
+ /** The server determined the sequence is terminal for this consumer. */
17
+ | {
7
18
  type: "ended";
8
19
  outcome: SequenceOutcome;
9
20
  };
21
+ /**
22
+ * Runtime monitor contract implemented by mechanism modules.
23
+ *
24
+ * Journey drivers start monitoring while a consumer is participating or
25
+ * settling, receive terminal `MonitorUpdate` callbacks, and stop monitoring
26
+ * when the journey leaves the active sequence. Display state is mechanism
27
+ * specific and is not part of this callback contract.
28
+ */
10
29
  export interface DistributionMonitor {
30
+ /**
31
+ * Begin observing a distribution or booking for this consumer.
32
+ *
33
+ * `id` is the distribution or appointment identifier understood by the
34
+ * underlying module. `context` is module-owned diagnostic/input data, and
35
+ * `onUpdate` is called when the monitor observes a grant or terminal outcome.
36
+ */
11
37
  startMonitoring(id: string, context?: Record<string, unknown>, onUpdate?: (update: MonitorUpdate) => void): void;
38
+ /** Stop observing the distribution or booking identified by `id`. */
12
39
  stopMonitoring(id: string): void;
40
+ /** Return whether the monitor currently owns an active subscription for `id`. */
13
41
  isMonitoring(id: string): boolean;
14
42
  }
43
+ /**
44
+ * Live queue display state written by the queue module and read by queue views.
45
+ *
46
+ * Values are ephemeral and may be absent until the monitor reports them. They
47
+ * are safe for UI display but are not durable journey facts.
48
+ */
15
49
  export interface QueueDisplayState {
50
+ /** Server-reported queue position; smaller values are closer to admission. */
16
51
  position?: number;
52
+ /** Estimated wait time in seconds when the queue service provides one. */
17
53
  estimatedWaitTime?: number;
54
+ /** Whether `position` should be rendered exactly or qualitatively. */
18
55
  positionDisplayMode?: "precise" | "qualitative";
19
56
  }
57
+ /**
58
+ * Live draw display state written by the draw module and read by draw views.
59
+ *
60
+ * Values are ephemeral and may lag the entry action; `/consumers/me` remains
61
+ * authoritative for durable consumer status and terminal outcome.
62
+ */
20
63
  export interface DrawDisplayState {
64
+ /** ISO 8601 timestamp for the scheduled draw settlement, when known. */
21
65
  drawTime?: string;
66
+ /** Server-issued entry number or label for this consumer's draw entry. */
22
67
  entryNumber?: string;
23
68
  }
69
+ /**
70
+ * Live auction display state written by the auction module and read by auction views.
71
+ *
72
+ * Money values are decimal strings in the distribution currency. Values are
73
+ * server-observed display facts and may update independently from durable
74
+ * journey state.
75
+ */
24
76
  export interface AuctionDisplayState {
77
+ /** Current highest bid for the auction as a decimal money string. */
25
78
  highestBid?: string;
79
+ /** This consumer's current bid as a decimal money string, when participating. */
26
80
  currentBid?: string;
81
+ /** Number of bids reported by the auction service. */
27
82
  bidCount?: number;
83
+ /** ISO 8601 timestamp when bidding closes, when known. */
28
84
  closeAt?: string;
85
+ /** Auction display status from the auction service. */
29
86
  status?: string;
87
+ /** ISO 4217 currency code for auction money fields. */
30
88
  currencyCode?: string;
89
+ /** Minimum legal next bid as a decimal money string. */
31
90
  minNextBid?: string;
91
+ /** Required bid increment as a decimal money string. */
32
92
  bidIncrement?: string;
33
93
  }
94
+ /**
95
+ * Live timed-release display state written by the timed-release module.
96
+ *
97
+ * Values are ephemeral. The admission credential itself is carried on the
98
+ * journey grant, not treated as durable display state.
99
+ */
34
100
  export interface TimedReleaseDisplayState {
101
+ /** ISO 8601 timestamp when this consumer entered the timed release. */
35
102
  enteredAt?: string;
103
+ /** Checkout URL surfaced by the timed-release service, when available. */
36
104
  checkoutUrl?: string;
37
105
  }
106
+ /**
107
+ * Live appointment display state written by the appointment module.
108
+ *
109
+ * Booking fields are server-authoritative display facts for the selected slot.
110
+ * Timestamps are ISO 8601 strings; absence means the current monitor response
111
+ * has not provided that value.
112
+ */
38
113
  export interface AppointmentDisplayState {
114
+ /** Current booking details for the consumer's appointment. */
39
115
  booking?: {
116
+ /** Booked slot identifier from the appointment service. */
40
117
  slotId: string;
118
+ /** Booked location identifier, when the appointment has locations. */
41
119
  locationId?: string;
120
+ /** Human-readable location name, when supplied by the server. */
42
121
  locationName?: string;
43
122
  /** Join URL for online appointments. Absent for in-person bookings. */
44
123
  joinUrl?: string;
124
+ /** ISO 8601 appointment start timestamp. */
45
125
  startTime: string;
126
+ /** ISO 8601 appointment end timestamp. */
46
127
  endTime: string;
128
+ /** Server confirmation code for the booking, when one is issued. */
47
129
  confirmationCode?: string;
130
+ /** Server booking status for this consumer. */
48
131
  consumerStatus: "booked" | "checked_in" | "completed";
49
132
  };
133
+ /** Milliseconds until the appointment starts, computed by the monitor. */
50
134
  msUntilStart?: number;
51
135
  }
136
+ /**
137
+ * Mechanism-discriminated display payload replicated between journey tabs.
138
+ *
139
+ * Display state is reactive UI data, not persisted journey state. The `type`
140
+ * discriminator identifies which mechanism-specific display fields are valid.
141
+ */
52
142
  export type DistributionDisplayState = (QueueDisplayState & {
53
143
  type: "queue";
54
144
  }) | (DrawDisplayState & {
@@ -1 +1 @@
1
- import{ConsumerMeResponseSchema as e}from"@fanfare-io/fanfare-sdk-contracts/consumer-me";import{RoutingDecisionV1Schema as t}from"@fanfare-io/fanfare-sdk-contracts/routing";import{createError as r}from"../core/errors.js";import{getLogger as s}from"../core/logger.js";import{parseResponse as i}from"../core/parse-response.js";import{getEventBus as n}from"../state/events.js";import{getSDKStore as c}from"../state/store.js";class a extends Error{constructor(e){super(e),this.name="NoSequenceAvailableError"}}class o extends Error{constructor(e){super(e),this.name="SequenceFullError"}}class u{constructor(e,t,r,i,c,a){this.logger=s(),this.events=n(),this.http=e,this.queues=t,this.draws=r,this.auctions=i,this.waitlists=c,this.timedReleases=a}get store(){return c()}async get(e){try{return this.logger.info("Getting experience details",{experienceId:e}),await this.http.get(`/experiences/${e}`)}catch(t){throw this.logger.error("Failed to get experience",{experienceId:e,error:t}),this.events.emit("experience:error",{experienceId:e,error:t instanceof Error?t.message:"Unknown error"}),t}}async enter(e){try{const t=this.store.activeExperiences[e];if(t)return this.logger.info("Already in experience",{experienceId:e}),t;if(!this.store.session)throw r.unauthorized("Not authenticated. Call auth.guest() or auth.login() first");this.logger.info("Entering experience",{experienceId:e});const s=await this.http.post(`/experiences/${e}/enter`);return this.store.addActiveExperience(e,s),this.events.emit("experience:entered",{experienceId:e,enteredAt:s.enteredAt}),s}catch(t){if(t&&"object"==typeof t&&"message"in t){const r=t.message.toLowerCase();if(r.includes("already entered")||r.includes("already in experience"))return this.logger.info("Already in experience (from API)",{experienceId:e}),this.store.activeExperiences[e]||{experienceId:e,enteredAt:/* @__PURE__ */(new Date).toISOString()}}throw this.logger.error("Failed to enter experience",{experienceId:e,error:t}),this.events.emit("experience:error",{experienceId:e,error:t instanceof Error?t.message:"Unknown error"}),t}}async leave(e){try{const r=this.store.activeExperiences[e];if(!r)return void this.logger.warn("Not in experience",{experienceId:e});if(this.logger.info("Leaving experience",{experienceId:e}),r.currentDistribution){const{type:e,id:s}=r.currentDistribution;try{switch(e){case"queue":await this.queues.leave(s);break;case"draw":await this.draws.leave(s)}}catch(t){this.logger.warn("Failed to leave distribution during experience leave",{type:e,id:s,error:t})}}if(r.waitlistId)try{await this.waitlists.leave(r.waitlistId)}catch(t){this.logger.warn("Failed to leave waitlist during experience leave",{waitlistId:r.waitlistId,error:t})}await this.http.delete(`/experiences/${e}/leave`),this.store.removeActiveExperience(e),this.events.emit("experience:left",{experienceId:e})}catch(t){throw this.logger.error("Failed to leave experience",{experienceId:e,error:t}),this.events.emit("experience:error",{experienceId:e,error:t instanceof Error?t.message:"Unknown error"}),t}}mapGate(e){switch(e.type){case"authentication":return{type:"authentication",status:"required"};case"access_code":return{type:"access_code",status:e.status};case"bot_check":return{type:"bot_check",status:e.status,challenge:e.challenge,mitigation:e.mitigation}}}mapSequence(e){return{id:e.id,name:e.name,experienceId:e.experienceId,priority:e.priority,color:e.color??""}}mapRoutingResult(e){const t=e.offers.filter(e=>"gated"===e.status).map(e=>({sequence:this.mapSequence(e.sequence),status:"gated",gates:e.gates.map(e=>this.mapGate(e))}));if("no_access"===e.outcome)return{outcome:"no_access",offers:t};const r=e.selected;return{outcome:e.outcome,selected:{sequence:this.mapSequence(r.sequence),status:r.status,gates:r.gates.map(e=>this.mapGate(e)),capabilityGrant:r.sequenceCapabilityGrant,capabilityGrantExpiresAt:r.sequenceCapabilityGrantExpiresAt},offers:t}}async findSequence(e,r){try{this.logger.info("Finding sequence",{experienceId:e,hasAccessCode:!!r});const s=await this.http.post(`/experiences/${e}/route`,r?{accessCode:r}:{}),n=i(t,s,{endpoint:`/experiences/${e}/route`},{throwOnFailure:!0});if(!n.ok)throw n.error;return this.mapRoutingResult(n.value)}catch(s){throw this.logger.error("Failed to find sequence",{experienceId:e,error:s}),s}}async validateSequenceAccess(e,t){try{this.logger.info("Validating sequence access",{sequenceId:e,hasAccessCode:!!t});const r=await this.http.post(`/sequences/${e}/validate`,t?{accessCode:t}:{});return{sequenceId:e,hasAccess:r.hasAccess,accessReasons:r.accessReasons,deniedReasons:r.deniedReasons}}catch(r){throw this.logger.error("Failed to validate sequence access",{sequenceId:e,error:r}),r}}async selectSequence(e){try{this.logger.info("Selecting sequence",{sequenceId:e});const t=Object.keys(this.store.activeExperiences)[0];if(!t)throw r.internalError("No active experience found for sequence selection");this.store.updateExperienceSession(t,{assignedSequenceId:e}),this.events.emit("experience:sequence-selected",{sequenceId:e})}catch(t){if(t&&"object"==typeof t&&"message"in t){const e=t.message.toLowerCase();if(e.includes("sequence full")||e.includes("capacity reached"))throw new o("Sequence is full")}throw this.logger.error("Failed to select sequence",{sequenceId:e,error:t}),t}}async getCurrentDistributions(e){try{this.logger.info("Getting distribution context",{sequenceId:e});return await this.http.get(`/sequences/${e}/distributions`)}catch(t){throw this.logger.error("Failed to get distribution context",{sequenceId:e,error:t}),t}}async enterDistribution(e){try{this.logger.info("Entering distribution",{type:e.type,id:e.id});const t=c(),r=Object.keys(t.activeExperiences)[0],s=r?t.activeExperiences[r]:null;let i;switch(e.type){case"queue":{const t=await this.queues.enter(e.id);i="DENIED"===t.status||"EXPIRED"===t.status||"LEFT"===t.status?"ended":"active";break}case"draw":{const t=await this.draws.enter(e.id);i="DENIED"===t.status||"NOT_ENTERED"===t.status?"ended":"active";break}case"auction":await this.auctions.enter(e.id),i="active";break;case"appointment":this.logger.warn("Appointment distribution not yet implemented",{id:e.id});break;case"timed_release":i="ENTERED"===(await this.timedReleases.enter(e.id)).status?"active":"ended";break;default:this.logger.warn("Unknown distribution type",{type:e.type})}s&&r&&t.updateExperienceSession(r,{currentDistribution:i?{type:e.type,id:e.id,status:i}:void 0})}catch(t){throw this.logger.error("Failed to enter distribution",{type:e.type,id:e.id,error:t}),t}}getActiveSession(){const e=Object.values(this.store.activeExperiences);return e.length>0?e[0]:null}isInExperience(e){return!!this.store.activeExperiences[e]}getSelectedSequence(){const e=this.getActiveSession();return e&&e.assignedSequenceId?{id:e.assignedSequenceId,name:"",experienceId:e.experienceId,priority:0,color:"#000000"}:null}async getMe(){const t=await this.http.get("/consumers/me"),r=i(e,t,{endpoint:"/consumers/me"},{throwOnFailure:!0});if(!r.ok)throw r.error;return r.value}destroy(){this.logger.debug("ExperienceManagementModule destroyed")}}export{u as ExperienceManagementModule,a as NoSequenceAvailableError,o as SequenceFullError};
1
+ import{ConsumerMeResponseSchema as e}from"@fanfare-io/fanfare-sdk-contracts/consumer-me";import{RoutingDecisionV1Schema as t}from"@fanfare-io/fanfare-sdk-contracts/routing";import{SequenceDistributionContextSchema as r}from"@fanfare-io/fanfare-sdk-contracts/sequence-distribution-context";import{createError as s}from"../core/errors.js";import{getLogger as i}from"../core/logger.js";import{parseResponse as n}from"../core/parse-response.js";import{getEventBus as o}from"../state/events.js";import{getSDKStore as c}from"../state/store.js";class a extends Error{constructor(e){super(e),this.name="NoSequenceAvailableError"}}class u extends Error{constructor(e){super(e),this.name="SequenceFullError"}}class d{constructor(e,t,r,s,n,c){this.logger=i(),this.events=o(),this.http=e,this.queues=t,this.draws=r,this.auctions=s,this.waitlists=n,this.timedReleases=c}get store(){return c()}async get(e){try{return this.logger.info("Getting experience details",{experienceId:e}),await this.http.get(`/experiences/${e}`)}catch(t){throw this.logger.error("Failed to get experience",{experienceId:e,error:t}),this.events.emit("experience:error",{experienceId:e,error:t instanceof Error?t.message:"Unknown error"}),t}}async enter(e){try{const t=this.store.activeExperiences[e];if(t)return this.logger.info("Already in experience",{experienceId:e}),t;if(!this.store.session)throw s.unauthorized("Not authenticated. Call auth.guest() or auth.login() first");this.logger.info("Entering experience",{experienceId:e});const r=await this.http.post(`/experiences/${e}/enter`);return this.store.addActiveExperience(e,r),this.events.emit("experience:entered",{experienceId:e,enteredAt:r.enteredAt}),r}catch(t){if(t&&"object"==typeof t&&"message"in t){const r=t.message.toLowerCase();if(r.includes("already entered")||r.includes("already in experience"))return this.logger.info("Already in experience (from API)",{experienceId:e}),this.store.activeExperiences[e]||{experienceId:e,enteredAt:/* @__PURE__ */(new Date).toISOString()}}throw this.logger.error("Failed to enter experience",{experienceId:e,error:t}),this.events.emit("experience:error",{experienceId:e,error:t instanceof Error?t.message:"Unknown error"}),t}}async leave(e){try{const r=this.store.activeExperiences[e];if(!r)return void this.logger.warn("Not in experience",{experienceId:e});if(this.logger.info("Leaving experience",{experienceId:e}),r.currentDistribution){const{type:e,id:s}=r.currentDistribution;try{switch(e){case"queue":await this.queues.leave(s);break;case"draw":await this.draws.leave(s)}}catch(t){this.logger.warn("Failed to leave distribution during experience leave",{type:e,id:s,error:t})}}if(r.waitlistId)try{await this.waitlists.leave(r.waitlistId)}catch(t){this.logger.warn("Failed to leave waitlist during experience leave",{waitlistId:r.waitlistId,error:t})}await this.http.delete(`/experiences/${e}/leave`),this.store.removeActiveExperience(e),this.events.emit("experience:left",{experienceId:e})}catch(t){throw this.logger.error("Failed to leave experience",{experienceId:e,error:t}),this.events.emit("experience:error",{experienceId:e,error:t instanceof Error?t.message:"Unknown error"}),t}}mapGate(e){switch(e.type){case"authentication":return{type:"authentication",status:"required"};case"access_code":return{type:"access_code",status:e.status};case"bot_check":return{type:"bot_check",status:e.status,challenge:e.challenge,mitigation:e.mitigation}}}mapSequence(e){return{id:e.id,name:e.name,experienceId:e.experienceId,priority:e.priority,color:e.color??""}}mapRoutingResult(e){const t=e.offers.filter(e=>"gated"===e.status).map(e=>({sequence:this.mapSequence(e.sequence),status:"gated",gates:e.gates.map(e=>this.mapGate(e))}));if("no_access"===e.outcome)return{outcome:"no_access",offers:t};const r=e.selected;return{outcome:e.outcome,selected:{sequence:this.mapSequence(r.sequence),status:r.status,gates:r.gates.map(e=>this.mapGate(e)),capabilityGrant:r.sequenceCapabilityGrant,capabilityGrantExpiresAt:r.sequenceCapabilityGrantExpiresAt},offers:t}}async findSequence(e,r){try{this.logger.info("Finding sequence",{experienceId:e,hasAccessCode:!!r});const s=await this.http.post(`/experiences/${e}/route`,r?{accessCode:r}:{}),i=n(t,s,{endpoint:`/experiences/${e}/route`},{throwOnFailure:!0});if(!i.ok)throw i.error;return this.mapRoutingResult(i.value)}catch(s){throw this.logger.error("Failed to find sequence",{experienceId:e,error:s}),s}}async validateSequenceAccess(e,t){try{this.logger.info("Validating sequence access",{sequenceId:e,hasAccessCode:!!t});const r=await this.http.post(`/sequences/${e}/validate`,t?{accessCode:t}:{});return{sequenceId:e,hasAccess:r.hasAccess,accessReasons:r.accessReasons,deniedReasons:r.deniedReasons}}catch(r){throw this.logger.error("Failed to validate sequence access",{sequenceId:e,error:r}),r}}async selectSequence(e){try{this.logger.info("Selecting sequence",{sequenceId:e});const t=Object.keys(this.store.activeExperiences)[0];if(!t)throw s.internalError("No active experience found for sequence selection");this.store.updateExperienceSession(t,{assignedSequenceId:e}),this.events.emit("experience:sequence-selected",{sequenceId:e})}catch(t){if(t&&"object"==typeof t&&"message"in t){const e=t.message.toLowerCase();if(e.includes("sequence full")||e.includes("capacity reached"))throw new u("Sequence is full")}throw this.logger.error("Failed to select sequence",{sequenceId:e,error:t}),t}}async getCurrentDistributions(e){try{this.logger.info("Getting distribution context",{sequenceId:e});const t=await this.http.get(`/sequences/${e}/distributions`),s=n(r,t,{endpoint:`/sequences/${e}/distributions`},{throwOnFailure:!0});if(!s.ok)throw s.error;return s.value}catch(t){throw this.logger.error("Failed to get distribution context",{sequenceId:e,error:t}),t}}async enterDistribution(e){try{this.logger.info("Entering distribution",{type:e.type,id:e.id});const t=c(),r=Object.keys(t.activeExperiences)[0],s=r?t.activeExperiences[r]:null;let i;switch(e.type){case"queue":{const t=await this.queues.enter(e.id);i="DENIED"===t.status||"EXPIRED"===t.status||"LEFT"===t.status?"ended":"active";break}case"draw":{const t=await this.draws.enter(e.id);i="DENIED"===t.status||"NOT_ENTERED"===t.status?"ended":"active";break}case"auction":await this.auctions.enter(e.id),i="active";break;case"appointment":this.logger.warn("Appointment distribution not yet implemented",{id:e.id});break;case"timed_release":i="ENTERED"===(await this.timedReleases.enter(e.id)).status?"active":"ended";break;default:this.logger.warn("Unknown distribution type",{type:e.type})}s&&r&&t.updateExperienceSession(r,{currentDistribution:i?{type:e.type,id:e.id,status:i}:void 0})}catch(t){throw this.logger.error("Failed to enter distribution",{type:e.type,id:e.id,error:t}),t}}getActiveSession(){const e=Object.values(this.store.activeExperiences);return e.length>0?e[0]:null}isInExperience(e){return!!this.store.activeExperiences[e]}getSelectedSequence(){const e=this.getActiveSession();return e&&e.assignedSequenceId?{id:e.assignedSequenceId,name:"",experienceId:e.experienceId,priority:0,color:"#000000"}:null}async getMe(){const t=await this.http.get("/consumers/me"),r=n(e,t,{endpoint:"/consumers/me"},{throwOnFailure:!0});if(!r.ok)throw r.error;return r.value}destroy(){this.logger.debug("ExperienceManagementModule destroyed")}}export{d as ExperienceManagementModule,a as NoSequenceAvailableError,u as SequenceFullError};
@@ -1,7 +1,8 @@
1
1
  import { ReadableAtom } from 'nanostores';
2
2
  import { DistributionDisplayState } from './distribution-monitor.types';
3
- import { AdmissionGrant, JourneySnapshot, JourneyView } from './journey.types';
4
- export interface JourneyViewHandlers {
3
+ import { AccessCodeSubmitResult, AdmissionGrant, JourneySnapshot, JourneyView, SequenceMechanism } from './journey.types';
4
+ import { SequenceDriver } from './sequence-driver';
5
+ interface JourneyShellHandlers {
5
6
  start(opts?: {
6
7
  accessCode?: string;
7
8
  reset?: boolean;
@@ -9,14 +10,11 @@ export interface JourneyViewHandlers {
9
10
  reroute(opts?: {
10
11
  accessCode?: string;
11
12
  }): Promise<void>;
13
+ submitAccessCode(accessCode: string): Promise<AccessCodeSubmitResult>;
12
14
  retry(): Promise<void>;
13
- enter(): Promise<void>;
14
- leave(): Promise<void>;
15
- bid(amount: string): Promise<void>;
16
- complete(): Promise<void>;
17
- book(slotId: string, locationId?: string): Promise<void>;
18
- cancel(reason?: string): Promise<void>;
19
- reschedule(newSlotId: string, newLocationId?: string): Promise<void>;
15
+ joinWaitlist(): Promise<void>;
16
+ exitWaitlist(): Promise<void>;
20
17
  claim(): AdmissionGrant;
21
18
  }
22
- export declare function buildJourneyView(snapshot: JourneySnapshot, handlers: JourneyViewHandlers, displayAtom: ReadableAtom<DistributionDisplayState> | null): JourneyView;
19
+ export declare function buildJourneyView(snapshot: JourneySnapshot, handlers: JourneyShellHandlers, displayAtom: ReadableAtom<DistributionDisplayState> | null, sequenceDrivers: Partial<Record<SequenceMechanism, SequenceDriver>>): JourneyView;
20
+ export {};
@@ -1 +1 @@
1
- function e(e,n,a){switch(e.journeyStage){case"ready":return{journeyStage:"ready",start:e=>n.start(e)};case"routing":return{journeyStage:"routing"};case"gated":return{journeyStage:"gated",gates:e.gates,selected:e.selected,reroute:e=>n.reroute(e),retry:()=>n.retry()};case"routed":return{journeyStage:"routed",sequenceId:e.sequenceId,offers:e.offers,reroute:()=>n.reroute(),retry:()=>n.retry(),sequence:t(e,n,a)}}}function t(e,t,n){const a=e.sequence;switch(a.phase){case"unavailable":return{phase:"unavailable",reason:a.reason};case"scheduled":return{phase:"scheduled",mechanism:a.mechanism,distribution:a.distribution,consumer:a.consumer,startsAt:a.distribution.startsAt??a.distribution.opensAt};case"enterable":return"appointment"===a.mechanism?{phase:"enterable",mechanism:"appointment",distribution:a.distribution,consumer:a.consumer,book:(e,n)=>t.book(e,n)}:"waitlist"===a.mechanism?{phase:"enterable",mechanism:"waitlist",target:a.target,consumer:a.consumer,enter:()=>t.enter()}:"auction"===a.mechanism?{phase:"enterable",mechanism:"auction",distribution:a.distribution,consumer:a.consumer,bid:e=>t.bid(e)}:{phase:"enterable",mechanism:a.mechanism,distribution:a.distribution,consumer:a.consumer,enter:()=>t.enter()};case"participating":return"waitlist"===a.mechanism?{phase:"participating",mechanism:"waitlist",consumer:a.consumer,leave:()=>t.leave()}:"appointment"===a.mechanism?{phase:"participating",mechanism:"appointment",consumer:a.consumer,state$:n,cancel:e=>t.cancel(e),reschedule:(e,n)=>t.reschedule(e,n)}:"auction"===a.mechanism?{phase:"participating",mechanism:"auction",consumer:a.consumer,state$:n,bid:e=>t.bid(e),leave:()=>t.leave()}:"timed_release"===a.mechanism?{phase:"participating",mechanism:"timed_release",consumer:a.consumer,state$:n,leave:()=>t.leave(),complete:()=>t.complete()}:"queue"===a.mechanism?{phase:"participating",mechanism:"queue",consumer:a.consumer,state$:n,leave:()=>t.leave()}:{phase:"participating",mechanism:"draw",consumer:a.consumer,state$:n,leave:()=>t.leave()};case"settling":return{phase:"settling",mechanism:a.mechanism,distribution:a.distribution,consumer:a.consumer};case"granted":return{phase:"granted",mechanism:a.mechanism,distribution:a.distribution,consumer:a.consumer,grant:a.grant,claim:()=>t.claim()};case"ended":return{phase:"ended",mechanism:a.mechanism,consumer:a.consumer,outcome:a.outcome}}throw new Error("Unknown sequence phase")}export{e as buildJourneyView};
1
+ function e(e,t,s,a){switch(e.journeyStage){case"ready":return{journeyStage:"ready",start:e=>t.start(e)};case"routing":return{journeyStage:"routing"};case"gated":return{journeyStage:"gated",gates:e.gates,selected:e.selected,reroute:e=>t.reroute(e),retry:()=>t.retry()};case"routed":return{journeyStage:"routed",sequenceId:e.sequenceId,offers:e.offers,reroute:()=>t.reroute(),submitAccessCode:e=>t.submitAccessCode(e),retry:()=>t.retry(),sequence:r(e,a,{displayAtom:s,claim:()=>t.claim(),joinWaitlist:()=>t.joinWaitlist(),exitWaitlist:()=>t.exitWaitlist()})}}}function r(e,r,t){const s=e.sequence;if("unavailable"===s.phase)return{phase:"unavailable",reason:s.reason};const a=r[s.mechanism];if(!a)throw new Error(`${s.mechanism} sequence driver is required`);return a.buildView(s,t)}export{e as buildJourneyView};
@@ -8,6 +8,8 @@ export declare class ExperienceJourney {
8
8
  private readonly sdk;
9
9
  private readonly stateAtom;
10
10
  private readonly store;
11
+ private readonly logger;
12
+ private readonly sequenceDrivers;
11
13
  readonly state: ReadableAtom<JourneySnapshot>;
12
14
  readonly view$: ReadableAtom<JourneyView>;
13
15
  readonly latestEvent$: ReadableAtom<JourneyEvent | null>;
@@ -15,6 +17,10 @@ export declare class ExperienceJourney {
15
17
  private pollTimer?;
16
18
  private activeMonitor;
17
19
  private grantExpiryTimer?;
20
+ private capabilityRefreshTimer?;
21
+ private lastCapabilityRefreshAtMs?;
22
+ private consecutiveRefreshFailures;
23
+ private checkoutStarted;
18
24
  private displayAtom;
19
25
  private destroyed;
20
26
  private operationQueue;
@@ -23,31 +29,37 @@ export declare class ExperienceJourney {
23
29
  getExperienceId(): string;
24
30
  private start;
25
31
  private reroute;
32
+ /**
33
+ * Routes with a candidate access code without persisting it up front.
34
+ * Routing is stateless and single-code, so the candidate replaces the stored
35
+ * code for the attempt: it is committed only when the journey advances into
36
+ * a previously offered sequence. If the candidate instead displaces the
37
+ * consumer from a code-held sequence (or gates the journey), the previously
38
+ * stored code is re-applied before reporting the outcome, and the consumer's
39
+ * in-sequence participation (consumer/grant/waitlist state) survives the
40
+ * round trip.
41
+ */
42
+ private submitAccessCode;
26
43
  private retry;
27
44
  destroy(): void;
28
45
  resumeFromMe(me: ConsumerMe): Promise<boolean>;
29
46
  refreshDistribution(): Promise<void>;
30
- private enterDistributionInternal;
31
- private enterActiveDistribution;
32
- private bookAppointmentSlot;
33
- private cancelAppointmentBooking;
34
- private rescheduleAppointmentBooking;
35
- private completeTimedRelease;
36
- private bid;
37
- private enterWait;
38
- private leaveWait;
39
- private enter;
40
- private leave;
41
- private leaveParticipation;
47
+ private joinWaitlistAttachment;
48
+ private exitWaitlistAttachment;
42
49
  private claim;
43
50
  ackEvent(eventId: string): void;
44
51
  ackAllEvents(): void;
45
52
  private routeAndLoadDistribution;
46
53
  private resolveRouting;
47
54
  private enterExperienceIfReady;
55
+ private createSequenceDrivers;
48
56
  private buildView;
49
- private mutate;
50
- private flattenSnapshot;
57
+ private projectSequenceState;
58
+ private patchJourneyBase;
59
+ private transitionToRouting;
60
+ private transitionToGated;
61
+ private baseFromSnapshot;
62
+ private commit;
51
63
  private emitEvent;
52
64
  private emitIfDistributionChanged;
53
65
  private buildDistributionSummary;
@@ -57,20 +69,35 @@ export declare class ExperienceJourney {
57
69
  private setSnapshot;
58
70
  private emitPhaseChanged;
59
71
  private runSerializedOperation;
60
- private transitionToParticipating;
61
72
  private transitionToGranted;
62
73
  private transitionToEnded;
74
+ /**
75
+ * A journey that held grant context and then observes the participation as
76
+ * absent (the server reports NOT_QUEUED / NOT_ENTERED, i.e. the standing
77
+ * register lapsed and was cleaned) renders ended(expired) — the admission
78
+ * window passed — rather than resetting to a fresh, re-enterable state. The
79
+ * wire status stays NOT_QUEUED; this is client-side interpretation that uses
80
+ * the held grant context to distinguish a lapsed admission from never having
81
+ * entered.
82
+ */
83
+ private reinterpretLapsedGrantOutcome;
63
84
  applyMonitorUpdate(update: MonitorUpdate): void;
85
+ private applyMonitorUpdateFromCallback;
64
86
  private startMonitorForParticipation;
65
- private getDistributionModule;
66
87
  stopRuntimeMonitoring(clearDisplay?: boolean): void;
67
88
  private clearGrantExpiryTimer;
89
+ private clearCapabilityRefreshTimer;
90
+ private armCapabilityRefreshTimer;
91
+ private scheduleCapabilityRefresh;
68
92
  private rearmTimersIfNeeded;
93
+ private refreshCapabilityFromTimer;
94
+ private expireGrantFromTimer;
95
+ private dropStaleCallbackCommit;
69
96
  getRuntimeBinding(): {
70
97
  experienceId: string;
71
98
  sequenceId: string;
72
99
  participationId: string;
73
- participationType: Exclude<Participation["type"], "waitlist">;
100
+ participationType: Participation["type"];
74
101
  } | null;
75
102
  startRuntimeMonitoring(): void;
76
103
  getRuntimeDisplayAtom(): WritableAtom<DistributionDisplayState> | null;
@@ -79,9 +106,7 @@ export declare class ExperienceJourney {
79
106
  private ensureDisplayAtom;
80
107
  private resetDisplayAtom;
81
108
  private clearRuntimeDisplayState;
82
- private enteredConsumer;
83
- private mapEnterResultToUpdate;
84
- private queueOutcome;
109
+ private clearRoutedRuntimeState;
85
110
  private deriveTerminalOutcomeForClosedDistribution;
86
111
  private withGrantedConsumer;
87
112
  private withTerminalConsumer;
@@ -1 +1 @@
1
- import{atom as e,computed as t}from"nanostores";import{createError as i}from"../core/errors.js";import{isMoneyString as s,addMoney as n}from"../core/money.js";import{generateId as o}from"../core/utils.js";import{clearExperienceContext as r,clearSequenceContext as a,bindDistributionContext as d,upsertSequenceToken as c}from"../state/capability-token-registry.js";import{getEventBus as u}from"../state/events.js";import{getSDKStore as p}from"../state/store.js";import{NoSequenceAvailableError as h}from"./experience.module.js";import{buildJourneyView as m}from"./journey-view.js";import{createInitialSnapshot as l,getParticipation as y,getSequencePhase as g,normalizeSequenceState as I,getSequenceId as f,getDistribution as w,getConsumer as v,getOutcome as b,deriveSequenceState as A,getGrant as E,getSequenceState as q,getTarget as x,getGates as T,buildSnapshot as S,getOffers as D,createGrant as k,getAdmissionGrantExpiresAt as C}from"./journey.machine.js";class j{constructor(i,s){this.experienceId=i,this.sdk=s,this.store=p(),this.activeMonitor=null,this.displayAtom=null,this.destroyed=!1,this.operationQueue=Promise.resolve(),this.EVENT_BUFFER_LIMIT=20;const n=this.restoreFromStore(),o=n??l(this.experienceId),r=n?y(n):void 0;r&&"participating"===g(o)&&this.ensureDisplayAtom(r),this.stateAtom=e(o),this.state=this.stateAtom,this.view$=t(this.stateAtom,e=>this.buildView(e)),this.events$=t(this.stateAtom,e=>e.events),this.latestEvent$=t(this.stateAtom,e=>{if(!e.events.length)return null;const t=e.lastSeenEventId??"",i=e.events.filter(e=>e.id>t);return i.length?i[i.length-1]:null}),n?this.rearmTimersIfNeeded():this.persist()}getExperienceId(){return this.experienceId}async start(e){await this.runSerializedOperation(async()=>{e?.reset&&this.setSnapshot(l(this.experienceId)),"accessCode"in(e??{})&&this.mutate({accessCode:e?.accessCode??null}),await this.routeAndLoadDistribution(this.stateAtom.get().accessCode)})}async reroute(e={}){await this.runSerializedOperation(async()=>{"accessCode"in e&&this.mutate({accessCode:e.accessCode??null}),await this.routeAndLoadDistribution(e.accessCode??this.stateAtom.get().accessCode)})}async retry(){await this.runSerializedOperation(()=>this.routeAndLoadDistribution(this.stateAtom.get().accessCode))}destroy(){this.destroyed||(this.destroyed=!0,this.stopRuntimeMonitoring(!0),this.clearGrantExpiryTimer(),this.stopPolling(),r(this.experienceId),this.store.removeJourney(this.experienceId),this.stateAtom.set(l(this.experienceId)))}async resumeFromMe(e){const t=e.active.journeys.find(e=>e.experienceId===this.experienceId&&"routed"===e.journeyStage);if(!t||"routed"!==t.journeyStage)return!1;const i=this.stateAtom.get(),s=I({sequence:t.sequence});this.mutate({journeyStage:"routed",sequenceId:t.sequenceId,sequence:s,offers:this.normalizeOffers(t.offers)}),"routed"===i.journeyStage&&i.sequenceId!==t.sequenceId&&a(this.experienceId,i.sequenceId);try{await this.reroute()}catch{}return this.rearmTimersIfNeeded(),!0}async refreshDistribution(){await this.runSerializedOperation(async()=>{const e=this.stateAtom.get(),t=f(e);if(!t)throw i.validationError("Cannot refresh distribution before routing to a sequence");const s=await this.sdk.experiences.getCurrentDistributions(t),n=this.buildDistributionSummary(s,t),o=w(e);n&&d({experienceId:this.experienceId,sequenceId:t,distributionContext:n});const r=v(e);let a="open"===n?.lifecycle&&"waitlist"===r?.mechanism?void 0:r;const c=this.deriveTerminalOutcomeForClosedDistribution(a,n,b(e));a=this.withTerminalConsumer(a,c);const u=A({distribution:n,consumer:a,grant:E(e),outcome:c});this.mutate({sequence:u,distribution:n,consumer:a,outcome:c}),this.emitIfDistributionChanged(o,n)})}async enterDistributionInternal(e){const t=this.stateAtom.get(),s=w(t);if(!s||"open"!==s.lifecycle||s.type!==e)throw i.validationError(`No open ${e.replace(/_/g," ")} to enter`);const n={queue:this.sdk.queues,draw:this.sdk.draws,auction:this.sdk.auctions,timed_release:this.sdk.timedReleases},o=await n[e].enter(s.id,void 0),r=this.mapEnterResultToUpdate(e,o);if(r)return void this.applyMonitorUpdate(r);const a=this.enteredConsumer(e,s.id);"auction"===e&&await this.sdk.auctions.status(s.id),this.ensureDisplayAtom({id:s.id,type:e}),this.transitionToParticipating(a,s),this.emitEvent("sequence_change","success","user",`Entered ${e.replace(/_/g," ")}`,{distributionId:s.id})}async enterActiveDistribution(){await this.runSerializedOperation(async()=>{const e=w(this.stateAtom.get());if(!e||"open"!==e.lifecycle)throw i.validationError("No open distribution to enter");if("appointment"===e.type)throw i.validationError("Appointments are entered via bookAppointmentSlot()");await this.enterDistributionInternal(e.type)})}async bookAppointmentSlot(e,t){await this.runSerializedOperation(async()=>{const s=w(this.stateAtom.get());if(!s||"open"!==s.lifecycle||"appointment"!==s.type)throw i.validationError("No open appointment to book");const n=await this.sdk.appointments.book(s.id,e,t);this.sdk.appointments.bumpMonitorGeneration(s.id);const o=this.ensureDisplayAtom({id:s.id,type:"appointment"}),r=Math.max(0,Date.parse(n.startTime)-Date.now());o.set({type:"appointment",booking:{slotId:n.slotId,locationId:n.locationId,locationName:n.locationName,joinUrl:n.joinUrl,startTime:n.startTime,endTime:n.endTime,confirmationCode:n.confirmationCode,consumerStatus:n.consumerStatus},msUntilStart:r});const a={mechanism:"appointment",status:"booked",id:s.id,distributionId:s.id,slotId:n.slotId,locationId:n.locationId};this.transitionToParticipating(a,s),this.emitEvent("sequence_change","success","user","Booked appointment slot",{appointmentId:s.id,slotId:e,locationId:t})})}async cancelAppointmentBooking(e){await this.runSerializedOperation(async()=>{const t=this.stateAtom.get(),s=v(t);if("appointment"!==s?.mechanism)throw i.validationError("Not participating in an appointment");const n=this.displayAtom?.get(),o=n&&"appointment"===n.type?n.booking:void 0;if(!o)throw i.validationError("No booking details available to cancel");this.stopRuntimeMonitoring(!0),await this.sdk.appointments.cancel(s.distributionId??s.id??"",o.slotId,o.locationId??"",e),this.transitionToEnded({type:"cancelled",reason:e})})}async rescheduleAppointmentBooking(e,t){await this.runSerializedOperation(async()=>{const s=v(this.stateAtom.get());if("appointment"!==s?.mechanism)throw i.validationError("Not participating in an appointment");const n=await this.sdk.appointments.reschedule(s.distributionId??s.id??"",e,t);this.sdk.appointments.bumpMonitorGeneration(s.distributionId??s.id??"");const o=this.ensureDisplayAtom({id:s.distributionId??s.id??"",type:"appointment"}),r=Math.max(0,Date.parse(n.startTime)-Date.now());o.set({type:"appointment",booking:{slotId:n.slotId,locationId:n.locationId,locationName:n.locationName,joinUrl:n.joinUrl,startTime:n.startTime,endTime:n.endTime,confirmationCode:n.confirmationCode,consumerStatus:n.consumerStatus},msUntilStart:r}),this.emitEvent("sequence_change","info","user","Rescheduled appointment",{appointmentId:s.distributionId??s.id,newSlotId:e,newLocationId:t})})}async completeTimedRelease(){await this.runSerializedOperation(async()=>{const e=this.stateAtom.get(),t=v(e);if("timed_release"!==t?.mechanism||"entered"!==t.status)throw i.validationError("Not participating in a timed release");const s=await this.sdk.timedReleases.complete(t.distributionId??t.id??"");s?this.transitionToGranted(s):this.transitionToEnded({type:"completed"}),this.emitEvent("sequence_change","success","user","Completed timed release",{timedReleaseId:t.distributionId??t.id})})}async bid(e){await this.runSerializedOperation(async()=>{const t=this.stateAtom.get(),o=v(t);if("auction"!==o?.mechanism)throw i.validationError("No auction is available to bid on");const r=w(t),a=q(t),d=o.distributionId??o.id??r?.id??"",c=await this.sdk.auctions.bid(d,e),u="winning"===c.status?"winning":"outbid"===c.status?"outbid":"bidding",p="auction"===r?.details?.type?r.details:void 0,h=p?.bidIncrement,m=c.highestBid&&h&&s(c.highestBid)&&s(h)?n(c.highestBid,h):void 0,l=this.ensureDisplayAtom({id:d,type:"auction"});"enterable"===a?.phase&&r&&this.transitionToParticipating({mechanism:"auction",status:u,id:d,distributionId:d,currentBid:e,highestBid:c.highestBid},r),l.set({type:"auction",currentBid:e,highestBid:c.highestBid,bidCount:c.bidCount,status:u,currencyCode:p?.currencyCode,bidIncrement:h,minNextBid:m,closeAt:p?.closeAt??p?.endsAt})})}async enterWait(){await this.runSerializedOperation(async()=>{const e=this.stateAtom.get(),t=f(e),s=w(e),n=x(e),o=n?.waitlistId??s?.waitlistId,r="string"==typeof o?o:void 0;if(!t||!r)throw i.validationError("Waitlist is not available for this sequence");const a={mechanism:"waitlist",status:"waitlisted",id:(await this.sdk.waitlists.enter(r)).id,waitlistId:r},d=A({distribution:s,target:n,consumer:a});this.mutate({sequence:d,consumer:a}),this.emitEvent("sequence_change","info","user","Entered waitlist",{waitlistId:r})})}async leaveWait(){await this.runSerializedOperation(async()=>{const e=this.stateAtom.get(),t=v(e),i=x(e),s="waitlist"===t?.mechanism?t.waitlistId??i?.waitlistId??t.id:void 0;if(!s)return;await this.sdk.waitlists.leave(s);const n=w(e),o=A({distribution:n,target:i});this.mutate({sequence:o}),this.emitEvent("sequence_change","info","user","Left waitlist",{waitlistId:s})})}async enter(){const e=q(this.stateAtom.get());"enterable"!==e?.phase||"waitlist"!==e.mechanism?await this.enterActiveDistribution():await this.enterWait()}async leave(){const e=v(this.stateAtom.get());"waitlist"!==e?.mechanism?await this.leaveParticipation():await this.leaveWait()}async leaveParticipation(){await this.runSerializedOperation(async()=>{const e=this.stateAtom.get(),t=y(e);t&&(this.stopRuntimeMonitoring(!0),"queue"===t.type?await this.sdk.queues.leave(t.id):"draw"===t.type?await this.sdk.draws.leave(t.id):"auction"===t.type?await this.sdk.auctions.leave(t.id):"timed_release"===t.type&&await this.sdk.timedReleases.leave(t.id),this.transitionToEnded({type:"left"}),this.emitEvent("sequence_change","info","user","Left participation",{type:t.type}))})}claim(){const e=E(this.stateAtom.get());if(!e)throw i.validationError("No grant is available to claim");return this.emitEvent("sequence_change","success","user","Grant claimed",{distributionType:e.distributionType,distributionId:e.distributionId,sequenceId:e.sequenceId}),u().emit("journey:grant-claimed",{experienceId:this.experienceId,sequenceId:e.sequenceId,grant:e.token}),e}ackEvent(e){this.stateAtom.get().events.some(t=>t.id===e)&&this.mutate({lastSeenEventId:e})}ackAllEvents(){const e=this.stateAtom.get();if(!e.events.length)return;const t=e.events[e.events.length-1];this.mutate({lastSeenEventId:t.id})}async routeAndLoadDistribution(e){const t=this.stateAtom.get(),i=f(t),s=await this.resolveRouting(e);if(!s||this.destroyed)return;if(await this.enterExperienceIfReady(),this.destroyed)return;const n=await this.sdk.experiences.getCurrentDistributions(s.sequenceId);if(this.destroyed)return;const o=this.buildDistributionSummary(n,s.sequenceId);o&&d({experienceId:this.experienceId,sequenceId:s.sequenceId,distributionContext:o});const r=i===s.sequenceId,a=r?v(t):void 0;let c="open"===o?.lifecycle&&"waitlist"===a?.mechanism?void 0:a;const u=r?E(t):void 0,p=r?this.deriveTerminalOutcomeForClosedDistribution(c,o,b(t)):void 0;c=this.withTerminalConsumer(c,p);const h=!o&&r&&"routed"===t.journeyStage&&("granted"===t.sequence.phase||"ended"===t.sequence.phase||"mechanism"in t.sequence&&"waitlist"===t.sequence.mechanism)?t.sequence:A({distribution:o,consumer:c,grant:u,outcome:p});this.mutate({journeyStage:"routed",sequenceId:s.sequenceId,distribution:o,offers:s.offers,sequence:h,consumer:c,grant:u,outcome:p});const m=r?w(t):void 0;this.emitIfDistributionChanged(m,o)}async resolveRouting(e){const t=this.stateAtom.get(),i=f(t);let s;this.mutate({journeyStage:"routing"});try{s=await this.sdk.experiences.findSequence(this.experienceId,e)}catch(o){throw this.setSnapshot(t),this.emitEvent("error","error","user","Failed to route experience"),o}if(this.destroyed)return null;if("gated"===s.outcome&&s.selected){const e=s.selected.gates;return this.mutate({journeyStage:"gated",gates:e,selected:{sequence:s.selected.sequence,status:"gated"}}),this.emitEvent("requirement","info","user","Gates required",{gates:e}),null}if("no_access"===s.outcome||!s.selected)throw this.setSnapshot(t),this.emitEvent("error","error","user","No access available for this experience"),new h("No access available for this experience");const n=s.selected.sequence.id;return i&&i!==n&&a(this.experienceId,i),s.selected.capabilityGrant&&c({experienceId:this.experienceId,sequenceId:n,token:s.selected.capabilityGrant,expiresAt:s.selected.capabilityGrantExpiresAt}),i!==n&&this.emitEvent("reroute","info","user","Sequence changed",{previousSequenceId:i,sequenceId:n}),{sequenceId:n,offers:s.offers}}async enterExperienceIfReady(){const e=this.stateAtom.get();if(T(e).length>0)return;if(e.experience)return;const t=await this.sdk.experiences.enter(this.experienceId);this.destroyed||this.mutate({experience:t})}buildView(e){return m(e,{start:e=>this.start(e),reroute:e=>this.reroute(e),retry:()=>this.retry(),enter:()=>this.enter(),leave:()=>this.leave(),bid:e=>this.bid(e),complete:()=>this.completeTimedRelease(),book:(e,t)=>this.bookAppointmentSlot(e,t),cancel:e=>this.cancelAppointmentBooking(e),reschedule:(e,t)=>this.rescheduleAppointmentBooking(e,t),claim:()=>this.claim()},this.displayAtom)}mutate(e){if(this.destroyed)return;const t=this.stateAtom.get(),i={...this.flattenSnapshot(t),...e},s=Object.prototype.hasOwnProperty.call(e,"accessCode")?i.accessCode??void 0:t.accessCode,n=Object.prototype.hasOwnProperty.call(e,"target")||"waitlist"===i.consumer?.mechanism?i.target??void 0:void 0,o=S({base:{revision:t.revision,updatedAt:t.updatedAt,experienceId:t.experienceId,accessCode:s,experience:i.experience,events:i.events??t.events,lastSeenEventId:i.lastSeenEventId??t.lastSeenEventId},journeyStage:i.journeyStage??t.journeyStage,gates:i.gates,selected:i.selected,sequenceId:i.sequenceId,distribution:i.distribution,target:n,offers:i.offers,sequence:i.sequence,consumer:i.consumer,grant:i.grant,outcome:i.outcome});this.setSnapshot(o)}flattenSnapshot(e){return{journeyStage:e.journeyStage,gates:T(e),selected:"gated"===e.journeyStage?e.selected:void 0,sequenceId:f(e),distribution:w(e),target:x(e),offers:D(e),sequence:"routed"===e.journeyStage?e.sequence:void 0,consumer:v(e),grant:E(e),outcome:b(e),accessCode:e.accessCode,experience:e.experience,events:e.events,lastSeenEventId:e.lastSeenEventId}}emitEvent(e,t,i,s,n){const r=this.stateAtom.get(),a={id:o(),ts:Date.now(),kind:e,severity:t,audience:i,message:s,detail:n},d=[...r.events,a],c=d.length>this.EVENT_BUFFER_LIMIT?d.slice(d.length-this.EVENT_BUFFER_LIMIT):d;this.mutate({events:c})}emitIfDistributionChanged(e,t){e?.id===t?.id&&e?.lifecycle===t?.lifecycle||this.emitEvent("distribution_change","info","user","Distribution context changed",{previous:e,next:t})}buildDistributionSummary(e,t){return e.active?{type:e.active.type,id:e.active.id,sequenceId:t,lifecycle:"open",details:e.active.details,opensAt:e.active.details.openAt,closesAt:e.active.details.closeAt,updatedAt:Date.now()}:e.upcoming?{type:e.upcoming.type,id:e.upcoming.id,sequenceId:t,lifecycle:"scheduled",startsAt:e.upcoming.startsAt,opensAt:e.upcoming.startsAt,waitlistId:e.upcoming.waitlistId,updatedAt:Date.now()}:e.recentPast?{type:e.recentPast.type,id:e.recentPast.id,sequenceId:t,lifecycle:"closed",closesAt:e.recentPast.endedAt,updatedAt:Date.now()}:void 0}normalizeOffers(e){return Array.isArray(e)?e.filter(Boolean):[]}restoreFromStore(){const e=this.store.activeJourneys;if(!e||!e[this.experienceId])return null;const t=e[this.experienceId];return z(t)?t:(this.store.removeJourney(this.experienceId),null)}persist(){this.store.setJourneyState(this.experienceId,this.stateAtom.get())}setSnapshot(e,t={}){if(this.destroyed)return;const i=this.stateAtom.get();this.stateAtom.set(e),!1!==t.persist&&this.persist(),this.emitPhaseChanged(i,e)}emitPhaseChanged(e,t){const i=e.journeyStage,s="routed"===i?g(e):void 0,n=t.journeyStage,o="routed"===n?g(t):void 0;i===n&&s===o||u().emit("journey:phase-changed",{experienceId:this.experienceId,journeyStage:n,sequencePhase:o})}runSerializedOperation(e){const t=this.operationQueue.then(async()=>{if(this.destroyed)throw i.notInitialized("Journey has been destroyed");return e()});return this.operationQueue=t.then(()=>{},()=>{}),t}transitionToParticipating(e,t){const i=A({distribution:t,consumer:e});this.mutate({sequence:i,consumer:e,distribution:t})}transitionToGranted(e,t){if(this.destroyed)return;const i=this.stateAtom.get(),s=w(i),n=f(i);if(!s||!n)return void this.transitionToEnded({type:"closed",reason:"missing_distribution"});if(void 0!==t&&Date.now()>=t)return void this.transitionToEnded({type:"expired"});const o=k({token:e,distribution:s,sequenceId:n,experienceId:this.experienceId,expiresAt:t}),r=this.withGrantedConsumer(v(i),s),a=A({distribution:s,consumer:r,grant:o});this.mutate({sequence:a,consumer:r,grant:o,outcome:void 0}),this.stopRuntimeMonitoring(!0),this.clearGrantExpiryTimer(),void 0!==t&&(this.grantExpiryTimer=setTimeout(()=>this.transitionToEnded({type:"expired"}),t-Date.now())),this.emitEvent("sequence_change","success","user","Grant issued",{grant:e}),u().emit("journey:granted",{experienceId:this.experienceId,sequenceId:n,grant:e})}transitionToEnded(e){if(this.destroyed)return;const t=this.stateAtom.get(),i=w(t),s={...e,distributionType:e.distributionType??i?.type,distributionId:e.distributionId??i?.id,at:e.at??/* @__PURE__ */(new Date).toISOString()},n=this.withTerminalConsumer(v(t),s),o=A({distribution:i,consumer:n,outcome:s});this.mutate({sequence:o,outcome:s,grant:void 0,consumer:n}),this.stopRuntimeMonitoring(!0),this.clearGrantExpiryTimer(),this.emitEvent("sequence_change","expired"===s.type?"warning":"info","user","Sequence ended",{outcome:s}),u().emit("journey:ended",{experienceId:this.experienceId,sequenceId:f(this.stateAtom.get()),outcome:s})}applyMonitorUpdate(e){this.destroyed||("granted"!==e.type?this.transitionToEnded(e.outcome):this.transitionToGranted(e.token,e.expiresAt))}startMonitorForParticipation(e){if("waitlist"===e.type)return;const t=this.stateAtom.get(),i=w(t),s={displayAtom:this.ensureDisplayAtom(e)};"draw"===e.type&&"draw"===i?.details?.type&&i.details.drawTime&&(s.drawTime=i.details.drawTime);const n=this.getDistributionModule(e.type);n.startMonitoring(e.id,s,e=>this.applyMonitorUpdate(e)),this.activeMonitor={type:e.type,id:e.id,stop:()=>n.stopMonitoring(e.id)}}getDistributionModule(e){switch(e){case"queue":return this.sdk.queues;case"draw":return this.sdk.draws;case"auction":return this.sdk.auctions;case"timed_release":return this.sdk.timedReleases;case"appointment":return this.sdk.appointments;default:throw new Error(`Unknown distribution type: ${e}`)}}stopRuntimeMonitoring(e=!1){this.activeMonitor&&(this.activeMonitor.stop(),this.activeMonitor=null),e&&this.clearRuntimeDisplayState()}clearGrantExpiryTimer(){this.grantExpiryTimer&&(clearTimeout(this.grantExpiryTimer),this.grantExpiryTimer=void 0)}rearmTimersIfNeeded(){if(this.destroyed)return;const e=this.stateAtom.get();if("granted"!==g(e)||this.grantExpiryTimer)return;const t=C(e);if(!t)return;const i=t-Date.now();i<=0?this.transitionToEnded({type:"expired"}):this.grantExpiryTimer=setTimeout(()=>this.transitionToEnded({type:"expired"}),i)}getRuntimeBinding(){const e=this.stateAtom.get(),t=g(e),i=y(e),s=f(e);return"participating"===t&&i&&s&&"waitlist"!==i.type?{experienceId:this.experienceId,sequenceId:s,participationId:i.id,participationType:i.type}:null}startRuntimeMonitoring(){const e=this.getRuntimeBinding();if(!e)return;const t=this.activeMonitor;t&&t.id===e.participationId&&t.type===e.participationType&&this.displayAtom||(this.stopRuntimeMonitoring(!1),this.startMonitorForParticipation({id:e.participationId,type:e.participationType}))}getRuntimeDisplayAtom(){return this.displayAtom}applyRuntimeState(e){if(!e)return void this.clearRuntimeDisplayState();const t=this.getRuntimeBinding();if(!t)return;this.ensureDisplayAtom({id:t.participationId,type:t.participationType}).set(e)}reconcileSyncedSnapshot(e){if(this.destroyed)return;const t=this.getRuntimeBinding();if(!e)return this.stopRuntimeMonitoring(!0),this.clearGrantExpiryTimer(),void this.setSnapshot(l(this.experienceId),{persist:!1});if(!z(e))return;const i=this.stateAtom.get();if(i.revision===e.revision&&i.updatedAt===e.updatedAt)return;const s=y(e),n=g(e),o="participating"===n&&s&&"waitlist"!==s.type?{sequenceId:f(e),participationId:s.id,participationType:s.type}:null;o?t&&t.sequenceId===o.sequenceId&&t.participationId===o.participationId&&t.participationType===o.participationType||this.resetDisplayAtom(o.participationType):this.displayAtom=null,this.setSnapshot(e,{persist:!1}),"participating"===n&&s?this.ensureDisplayAtom(s):this.stopRuntimeMonitoring(!0),this.clearGrantExpiryTimer(),this.rearmTimersIfNeeded()}ensureDisplayAtom(e){return this.resetDisplayAtom(e.type),this.displayAtom}resetDisplayAtom(t){this.displayAtom&&this.displayAtom.get().type===t?this.displayAtom.set({type:t}):this.displayAtom=e({type:t})}clearRuntimeDisplayState(){const e=this.getRuntimeBinding();e?this.resetDisplayAtom(e.participationType):this.displayAtom=null}enteredConsumer(e,t){switch(e){case"queue":return{mechanism:"queue",status:"queued",id:t,distributionId:t};case"draw":return{mechanism:"draw",status:"entered",id:t,distributionId:t};case"auction":return{mechanism:"auction",status:"bidding",id:t,distributionId:t};case"timed_release":return{mechanism:"timed_release",status:"entered",id:t,distributionId:t}}}mapEnterResultToUpdate(e,t){if("auction"===e||void 0===t)return null;if("queue"===e){const e=t;return"QUEUED"===e.status?null:"GRANTED"===e.status||"COMPLETED"===e.status?{type:"granted",token:e.admissionGrant,expiresAt:"expiresAt"in e&&e.expiresAt?new Date(e.expiresAt).getTime():void 0}:{type:"ended",outcome:{type:this.queueOutcome(e),reason:"reason"in e?e.reason:void 0}}}if("draw"===e){const e=t;return"ENTERED"===e.status?null:"WON"===e.status||"COMPLETED"===e.status?{type:"granted",token:e.admissionGrant,expiresAt:e.expiresAt?new Date(e.expiresAt).getTime():void 0}:{type:"ended",outcome:{type:"DENIED"===e.status?"denied":"lost",reason:"reason"in e?e.reason:void 0}}}const i=t;return"ENTERED"===i.status?null:"COMPLETED"===i.status?{type:"ended",outcome:{type:"completed"}}:{type:"ended",outcome:{type:"LEFT"===i.status?"left":"denied",reason:"reason"in i?i.reason:void 0}}}queueOutcome(e){return"LEFT"===e.status?"left":"EXPIRED"===e.status?"expired":"COMPLETED"===e.status?"completed":"DENIED"===e.status?"denied":"closed"}deriveTerminalOutcomeForClosedDistribution(e,t,i){if(i)return i;if(t&&"closed"===t.lifecycle&&e)switch(e.mechanism){case"draw":case"auction":return{type:"lost",distributionType:t.type,distributionId:t.id};case"waitlist":return{type:"expired",distributionType:t.type,distributionId:t.id};default:return{type:"closed",distributionType:t.type,distributionId:t.id}}}withGrantedConsumer(e,t){switch(e?.mechanism){case"queue":return{...e,status:"granted"};case"draw":case"auction":return{...e,status:"won"};case"timed_release":return{...e,status:"completed"};case void 0:if("queue"===t.type)return{mechanism:"queue",status:"granted",id:t.id,distributionId:t.id};if("draw"===t.type)return{mechanism:"draw",status:"won",id:t.id,distributionId:t.id};if("auction"===t.type)return{mechanism:"auction",status:"won",id:t.id,distributionId:t.id};if("timed_release"===t.type)return{mechanism:"timed_release",status:"completed",id:t.id,distributionId:t.id};throw i.validationError("Distribution does not produce grants");default:if(!e)throw i.validationError("Grant requires a consumer state");return e}}withTerminalConsumer(e,t){if(e){if(!t)return e;switch(e.mechanism){case"queue":return"left"===t.type?{...e,status:"left"}:"denied"===t.type?{...e,status:"denied",deniedReason:t.reason}:"completed"===t.type?{...e,status:"completed"}:{...e,status:"not_queued"};case"draw":return"denied"===t.type?{...e,status:"denied",deniedReason:t.reason}:"completed"===t.type?{...e,status:"completed"}:"won"===t.type?{...e,status:"won"}:{...e,status:"not_entered"};case"auction":return"won"===t.type?{...e,status:"won"}:{...e,status:"lost"};case"timed_release":return"left"===t.type?{...e,status:"left"}:"denied"===t.type?{...e,status:"denied",deniedReason:t.reason}:"completed"===t.type?{...e,status:"completed"}:{...e,status:"not_entered"};case"appointment":return"cancelled"===t.type?{...e,status:"cancelled"}:"no_show"===t.type?{...e,status:"no_show"}:"completed"===t.type?{...e,status:"completed"}:{...e,status:"not_booked"};case"waitlist":return"left"===t.type?{...e,status:"left"}:{...e,status:"expired"}}}}stopPolling(){this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=void 0)}}const R=["revision","updatedAt","experienceId","accessCode","experience","events","lastSeenEventId","journeyStage"],M={ready:new Set(R),routing:new Set(R),gated:/* @__PURE__ */new Set([...R,"gates","selected"]),routed:/* @__PURE__ */new Set([...R,"sequenceId","offers","sequence"])},_=/* @__PURE__ */new Set(["sequence","status"]),O=/* @__PURE__ */new Set(["phase","reason","mechanism","distribution","target","consumer","grant","outcome"]),N=/* @__PURE__ */new Set(["type","id","sequenceId","lifecycle","startsAt","opensAt","closesAt","settlesAt","waitlistId","details","updatedAt"]),B=/* @__PURE__ */new Set(["status","id","distributionId","waitlistId","enteredAt","admissionGrant","grantedAt","deniedReason","position","highestBid","currentBid","slotId","locationId","bookedAt","checkedInAt","completedAt","cancelledAt","noShowAt","notifiedAt"]),P=/* @__PURE__ */new Set(["token","distributionType","distributionId","sequenceId","experienceId","issuedAt","expiresAt","handoffUrl"]),G=/* @__PURE__ */new Set(["type","reason","at","distributionType","distributionId"]);function U(e,t){return!!e&&"object"==typeof e&&!Array.isArray(e)&&Object.keys(e).every(e=>t.has(e))}function z(e){if(!e||"object"!=typeof e)return!1;const t=e,i=t.journeyStage;if("ready"!==i&&"routing"!==i&&"gated"!==i&&"routed"!==i)return!1;if(!U(t,M[i]))return!1;if(void 0!==t.selected&&!U(t.selected,_))return!1;if("routed"!==i)return!0;const s=t.sequence;if(!function(e){return!(!U(e,O)||"distribution"in e&&!U(e.distribution,N)||"target"in e&&!U(e.target,N)||"consumer"in e&&!U(e.consumer,B)||"grant"in e&&!U(e.grant,P)||"outcome"in e&&!U(e.outcome,G))}(s))return!1;const n=s.phase;return"unavailable"===n||"scheduled"===n||"enterable"===n||"participating"===n||"settling"===n||"granted"===n||"ended"===n}export{j as ExperienceJourney};
1
+ import{atom as e,computed as t}from"nanostores";import*as i from"valibot";import{createAppointmentDriver as s,withAppointmentTerminalConsumer as r}from"../appointments/appointment.driver.js";import{createAuctionDriver as n,withAuctionGrantedConsumer as o,withAuctionTerminalConsumer as a}from"../auctions/auction.driver.js";import{createError as c,isFanfareError as d,ErrorCodes as u}from"../core/errors.js";import{createLogger as h}from"../core/logger.js";import{generateId as p}from"../core/utils.js";import{createDrawDriver as l,withDrawGrantedConsumer as m,withDrawTerminalConsumer as y}from"../draws/draw.driver.js";import{createQueueDriver as g,withQueueGrantedConsumer as f,withQueueTerminalConsumer as b}from"../queues/queue.driver.js";import{clearExperienceContext as I,clearSequenceContext as v,bindDistributionContext as S,upsertSequenceToken as A,getSequenceTokenExpiryMs as q}from"../state/capability-token-registry.js";import{getEventBus as w}from"../state/events.js";import{getSDKStore as x}from"../state/store.js";import{createTimedReleaseDriver as T,withTimedReleaseGrantedConsumer as R,withTimedReleaseTerminalConsumer as j}from"../timed-releases/timed-release.driver.js";import{NoSequenceAvailableError as C}from"./experience.module.js";import{buildJourneyView as E}from"./journey-view.js";import{createInitialSnapshot as D,getParticipation as k,getSequencePhase as M,normalizeSequenceState as O,buildSnapshot as F,getSequenceId as G,getDistribution as _,getConsumer as z,getWaitlistAttachment as B,getOutcome as U,getGrant as J,getSequenceState as P,getGates as L,createGrant as N,getAdmissionGrantExpiresAt as V,validateSequenceState as W}from"./journey.machine.js";const $="Cannot commit a stale sequence transition",Q="Cannot commit a sequence transition before routing",H=3e4;class K{constructor(i,s){this.experienceId=i,this.sdk=s,this.store=x(),this.logger=h(),this.activeMonitor=null,this.consecutiveRefreshFailures=0,this.checkoutStarted=!1,this.displayAtom=null,this.destroyed=!1,this.operationQueue=Promise.resolve(),this.EVENT_BUFFER_LIMIT=20;const r=this.restoreFromStore(),n=r??D(this.experienceId),o=r?k(r):void 0;o&&"participating"===M(n)&&this.ensureDisplayAtom(o),this.stateAtom=e(n),this.state=this.stateAtom,this.sequenceDrivers=this.createSequenceDrivers(),this.view$=t(this.stateAtom,e=>this.buildView(e)),this.events$=t(this.stateAtom,e=>e.events),this.latestEvent$=t(this.stateAtom,e=>{if(!e.events.length)return null;const t=e.lastSeenEventId??"",i=e.events.filter(e=>e.id>t);return i.length?i[i.length-1]:null}),r?this.rearmTimersIfNeeded():this.persist()}getExperienceId(){return this.experienceId}async start(e){await this.runSerializedOperation(async()=>{e?.reset&&(this.clearRoutedRuntimeState(),this.setSnapshot(D(this.experienceId))),"accessCode"in(e??{})&&this.patchJourneyBase({accessCode:e?.accessCode??null}),await this.routeAndLoadDistribution(this.stateAtom.get().accessCode)})}async reroute(e={}){await this.runSerializedOperation(async()=>{"accessCode"in e&&this.patchJourneyBase({accessCode:e.accessCode??null}),await this.routeAndLoadDistribution(e.accessCode??this.stateAtom.get().accessCode)})}async submitAccessCode(e){return await this.runSerializedOperation(async()=>{const t=this.stateAtom.get();if("routed"!==t.journeyStage){await this.routeAndLoadDistribution(e,{transient:!0});const t=this.stateAtom.get();return"routed"!==t.journeyStage?"gated"===t.journeyStage?{outcome:"gated"}:{outcome:"unchanged"}:(this.patchJourneyBase({accessCode:e}),{outcome:"advanced",sequenceId:t.sequenceId})}const i=t.sequenceId,s=t.accessCode,r=new Set(t.offers.map(e=>e.sequence.id));await this.routeAndLoadDistribution(e,{transient:!0});let n=this.stateAtom.get();return"routed"===n.journeyStage&&n.sequenceId===i?{outcome:"unchanged"}:"routed"===n.journeyStage&&r.has(n.sequenceId)?(this.patchJourneyBase({accessCode:e}),{outcome:"advanced",sequenceId:n.sequenceId}):("string"==typeof s&&s!==e&&(await this.routeAndLoadDistribution(s,{carryFrom:t,transient:!0}),n=this.stateAtom.get()),"gated"===n.journeyStage?{outcome:"gated"}:{outcome:"unchanged"})})}async retry(){await this.runSerializedOperation(()=>this.routeAndLoadDistribution(this.stateAtom.get().accessCode))}destroy(){this.destroyed||(this.destroyed=!0,this.stopRuntimeMonitoring(!0),this.clearGrantExpiryTimer(),this.clearCapabilityRefreshTimer(),this.stopPolling(),I(this.experienceId),this.store.removeJourney(this.experienceId),this.stateAtom.set(D(this.experienceId)))}async resumeFromMe(e){const t=e.active.journeys.find(e=>e.experienceId===this.experienceId&&"routed"===e.journeyStage);if(!t||"routed"!==t.journeyStage)return!1;const i=this.stateAtom.get(),s=O({sequence:t.sequence}),r=F({base:this.baseFromSnapshot(i),journeyStage:"routed",sequenceId:t.sequenceId,offers:this.normalizeOffers(t.offers),sequence:s});this.setSnapshot(r),"routed"===i.journeyStage&&i.sequenceId!==t.sequenceId&&v(this.experienceId,i.sequenceId);try{await this.reroute()}catch{}return this.rearmTimersIfNeeded(),!0}async refreshDistribution(){await this.runSerializedOperation(async()=>{const e=this.stateAtom.get(),t=G(e);if(!t)throw c.validationError("Cannot refresh distribution before routing to a sequence");const i=await this.sdk.experiences.getCurrentDistributions(t),s=this.buildDistributionSummary(i,t),r=_(e);s&&S({experienceId:this.experienceId,sequenceId:t,distributionContext:s});const n=z(e),o="scheduled"===s?.lifecycle?X(B(e),s):void 0,a=this.deriveTerminalOutcomeForClosedDistribution(n,s,U(e)),d=this.withTerminalConsumer(n,a),u=this.projectSequenceState({distribution:s,consumer:d,waitlist:o,grant:J(e),outcome:a});this.commit(u),this.emitIfDistributionChanged(r,s)})}async joinWaitlistAttachment(){await this.runSerializedOperation(async()=>{const e=this.stateAtom.get(),t=P(e),i=_(e),s="scheduled"===t?.phase&&"waitlist"in t?t.waitlist?.waitlistId:i?.waitlistId,r="string"==typeof s?s:void 0;if("scheduled"!==t?.phase||!i||!r)throw c.validationError("Waitlist is not available for this sequence");const n=await this.sdk.waitlists.enter(r),o=this.projectSequenceState({distribution:i,waitlist:{waitlistId:r,status:"waitlisted",entryId:n.id}});this.commit(o),this.emitEvent("sequence_change","info","user","Entered waitlist",{waitlistId:r})})}async exitWaitlistAttachment(){await this.runSerializedOperation(async()=>{const e=this.stateAtom.get(),t=P(e),i=_(e),s="scheduled"===t?.phase&&"waitlist"in t?t.waitlist:void 0,r=s?.waitlistId??i?.waitlistId;if("scheduled"!==t?.phase||!i||!r)return;await this.sdk.waitlists.leave(r);const n=this.projectSequenceState({distribution:i,waitlist:{waitlistId:r,status:"not_waitlisted"}});this.commit(n),this.emitEvent("sequence_change","info","user","Left waitlist",{waitlistId:r})})}claim(){const e=J(this.stateAtom.get());if(!e)throw c.validationError("No grant is available to claim");return this.checkoutStarted=!0,this.clearGrantExpiryTimer(),this.clearCapabilityRefreshTimer(),this.emitEvent("sequence_change","success","user","Grant claimed",{distributionType:e.distributionType,distributionId:e.distributionId,sequenceId:e.sequenceId}),w().emit("journey:grant-claimed",{experienceId:this.experienceId,sequenceId:e.sequenceId,grant:e.token}),e}ackEvent(e){this.stateAtom.get().events.some(t=>t.id===e)&&this.patchJourneyBase({lastSeenEventId:e})}ackAllEvents(){const e=this.stateAtom.get();if(!e.events.length)return;const t=e.events[e.events.length-1];this.patchJourneyBase({lastSeenEventId:t.id})}async routeAndLoadDistribution(e,t){const i=t?.carryFrom??this.stateAtom.get(),s=G(i),r=await this.resolveRouting(e,{transient:t?.transient});if(!r||this.destroyed)return;if(await this.enterExperienceIfReady(),this.destroyed)return;const n=await this.sdk.experiences.getCurrentDistributions(r.sequenceId);if(this.destroyed)return;const o=this.buildDistributionSummary(n,r.sequenceId);o&&S({experienceId:this.experienceId,sequenceId:r.sequenceId,distributionContext:o});const a=s===r.sequenceId,c=a?z(i):void 0,d=a&&"scheduled"===o?.lifecycle?X(B(i),o):void 0,u=a?J(i):void 0,h=a?this.deriveTerminalOutcomeForClosedDistribution(c,o,U(i)):void 0,p=this.withTerminalConsumer(c,h),l=!o&&a&&"routed"===i.journeyStage&&("granted"===i.sequence.phase||"ended"===i.sequence.phase)?i.sequence:this.projectSequenceState({distribution:o,consumer:p,waitlist:d,grant:u,outcome:h});a||(this.stopRuntimeMonitoring(!0),this.clearGrantExpiryTimer(),this.clearCapabilityRefreshTimer());const m=this.stateAtom.get(),y=F({base:this.baseFromSnapshot(m),journeyStage:"routed",sequenceId:r.sequenceId,offers:r.offers,sequence:l,sequenceRef:r.selectedSequence});this.setSnapshot(y),this.consecutiveRefreshFailures=0,this.rearmTimersIfNeeded();const g=a?_(i):void 0;this.emitIfDistributionChanged(g,o)}async resolveRouting(e,t){const i=this.stateAtom.get(),s=G(i);let r;this.transitionToRouting();try{r=await this.sdk.experiences.findSequence(this.experienceId,e)}catch(o){throw this.setSnapshot(i),"routed"===i.journeyStage&&this.armCapabilityRefreshTimer(),this.emitEvent("error","error","user","Failed to route experience"),o}if(this.destroyed)return null;if("gated"===r.outcome&&r.selected){const e=r.selected.gates;return"routed"===i.journeyStage&&this.clearRoutedRuntimeState(),this.transitionToGated({gates:e,selected:{sequence:r.selected.sequence,status:"gated"}}),this.emitEvent("requirement","info","user","Gates required",{gates:e}),null}if("no_access"===r.outcome||!r.selected)throw this.setSnapshot(i),this.emitEvent("error","error","user","No access available for this experience"),new C("No access available for this experience");const n=r.selected.sequence.id;if(s&&s!==n&&v(this.experienceId,s),r.selected.capabilityGrant&&A({experienceId:this.experienceId,sequenceId:n,token:r.selected.capabilityGrant,expiresAt:r.selected.capabilityGrantExpiresAt}),s!==n){const e="routed"===i.journeyStage?i.sequenceRef?.priority:void 0,o=r.selected.sequence.priority,a=function(e,t){return void 0===e?"unknown":t>e?"upgrade":t<e?"downgrade":"lateral"}(e,o);this.emitEvent("reroute","downgrade"===a?"warning":"info",t?.transient?"system":"user","downgrade"===a?"Sequence downgraded":"Sequence changed",{previousSequenceId:s,sequenceId:n,...void 0===e?{}:{previousPriority:e},priority:o,direction:a})}return{sequenceId:n,selectedSequence:r.selected.sequence,offers:r.offers}}async enterExperienceIfReady(){const e=this.stateAtom.get();if(L(e).length>0)return;if(e.experience)return;const t=await this.sdk.experiences.enter(this.experienceId);this.destroyed||this.patchJourneyBase({experience:t})}createSequenceDrivers(){return{appointment:s({appointments:this.sdk.appointments,runSerializedOperation:e=>this.runSerializedOperation(e),getCurrentSequence:()=>P(this.stateAtom.get()),getDisplayState:()=>this.displayAtom?.get()??null,commit:(e,t)=>this.commit(e,t),applyMonitorUpdate:e=>this.applyMonitorUpdate(e),ensureDisplayAtom:e=>this.ensureDisplayAtom(e),stopRuntimeMonitoring:e=>this.stopRuntimeMonitoring(e),emitEvent:(e,t,i,s,r)=>this.emitEvent(e,t,i,s,r)}),auction:n({auctions:this.sdk.auctions,runSerializedOperation:e=>this.runSerializedOperation(e),getCurrentSequence:()=>P(this.stateAtom.get()),commit:(e,t)=>this.commit(e,t),applyMonitorUpdate:e=>this.applyMonitorUpdate(e),ensureDisplayAtom:e=>this.ensureDisplayAtom(e),stopRuntimeMonitoring:e=>this.stopRuntimeMonitoring(e),emitEvent:(e,t,i,s,r)=>this.emitEvent(e,t,i,s,r)}),queue:g({queues:this.sdk.queues,runSerializedOperation:e=>this.runSerializedOperation(e),getCurrentSequence:()=>P(this.stateAtom.get()),commit:(e,t)=>this.commit(e,t),applyMonitorUpdate:e=>this.applyMonitorUpdate(e),ensureDisplayAtom:e=>{this.ensureDisplayAtom(e)},stopRuntimeMonitoring:e=>this.stopRuntimeMonitoring(e),emitEvent:(e,t,i,s,r)=>this.emitEvent(e,t,i,s,r)}),timed_release:T({timedReleases:this.sdk.timedReleases,runSerializedOperation:e=>this.runSerializedOperation(e),getCurrentSequence:()=>P(this.stateAtom.get()),commit:(e,t)=>this.commit(e,t),applyMonitorUpdate:e=>this.applyMonitorUpdate(e),ensureDisplayAtom:e=>{this.ensureDisplayAtom(e)},stopRuntimeMonitoring:e=>this.stopRuntimeMonitoring(e),emitEvent:(e,t,i,s,r)=>this.emitEvent(e,t,i,s,r)}),draw:l({draws:this.sdk.draws,runSerializedOperation:e=>this.runSerializedOperation(e),getCurrentSequence:()=>P(this.stateAtom.get()),commit:(e,t)=>this.commit(e,t),applyMonitorUpdate:e=>this.applyMonitorUpdate(e),ensureDisplayAtom:e=>{this.ensureDisplayAtom(e)},stopRuntimeMonitoring:e=>this.stopRuntimeMonitoring(e),emitEvent:(e,t,i,s,r)=>this.emitEvent(e,t,i,s,r)})}}buildView(e){return E(e,{start:e=>this.start(e),reroute:e=>this.reroute(e),submitAccessCode:e=>this.submitAccessCode(e),retry:()=>this.retry(),joinWaitlist:()=>this.joinWaitlistAttachment(),exitWaitlist:()=>this.exitWaitlistAttachment(),claim:()=>this.claim()},this.displayAtom,this.sequenceDrivers)}projectSequenceState(e){if(!e.distribution)return{phase:"unavailable"};const t=this.sequenceDrivers[e.distribution.type];if(!t)throw new Error(`${e.distribution.type} sequence driver is required`);return t.project(e)}patchJourneyBase(e){if(this.destroyed)return;const t=this.stateAtom.get(),i=this.baseFromSnapshot(t,e);switch(t.journeyStage){case"ready":return void this.setSnapshot(F({base:i,journeyStage:"ready"}));case"routing":return void this.setSnapshot(F({base:i,journeyStage:"routing"}));case"gated":return void this.setSnapshot(F({base:i,journeyStage:"gated",gates:t.gates,selected:t.selected}));case"routed":return void this.setSnapshot(F({base:i,journeyStage:"routed",sequenceId:t.sequenceId,offers:t.offers,sequence:t.sequence,sequenceRef:t.sequenceRef}))}}transitionToRouting(){if(this.destroyed)return;const e=this.stateAtom.get();this.setSnapshot(F({base:this.baseFromSnapshot(e),journeyStage:"routing"}))}transitionToGated(e){if(this.destroyed)return;const t=this.stateAtom.get();this.setSnapshot(F({base:this.baseFromSnapshot(t),journeyStage:"gated",gates:e.gates,selected:e.selected}))}baseFromSnapshot(e,t={}){const i=Object.prototype.hasOwnProperty.call(t,"accessCode")?t.accessCode??void 0:e.accessCode;return{revision:e.revision,updatedAt:e.updatedAt,experienceId:e.experienceId,accessCode:i,experience:t.experience??e.experience,events:t.events??e.events,lastSeenEventId:t.lastSeenEventId??e.lastSeenEventId}}commit(e,t={}){if(this.destroyed)throw c.notInitialized("Journey has been destroyed");const i=this.stateAtom.get();if("routed"!==i.journeyStage)throw c.validationError(Q);if(!function(e,t){if("distribution"in e&&e.distribution.sequenceId&&e.distribution.sequenceId!==t)return!1;return!0}(e,i.sequenceId))throw c.validationError($);const s=F({base:{revision:i.revision,updatedAt:i.updatedAt,experienceId:i.experienceId,accessCode:i.accessCode,experience:i.experience,events:t.events??i.events,lastSeenEventId:i.lastSeenEventId},journeyStage:"routed",sequenceId:i.sequenceId,offers:i.offers,sequence:e,sequenceRef:i.sequenceRef});this.setSnapshot(s)}emitEvent(e,t,i,s,r){const n=this.stateAtom.get(),o={id:p(),ts:Date.now(),kind:e,severity:t,audience:i,message:s,detail:r},a=[...n.events,o],c=a.length>this.EVENT_BUFFER_LIMIT?a.slice(a.length-this.EVENT_BUFFER_LIMIT):a;this.patchJourneyBase({events:c})}emitIfDistributionChanged(e,t){e?.id===t?.id&&e?.lifecycle===t?.lifecycle||this.emitEvent("distribution_change","info","user","Distribution context changed",{previous:e,next:t})}buildDistributionSummary(e,t){return e.active?{type:e.active.type,id:e.active.id,sequenceId:t,lifecycle:"open",details:e.active.details,opensAt:e.active.details.openAt,closesAt:e.active.details.closeAt,updatedAt:Date.now()}:e.upcoming?{type:e.upcoming.type,id:e.upcoming.id,sequenceId:t,lifecycle:"scheduled",startsAt:e.upcoming.startsAt,opensAt:e.upcoming.startsAt,waitlistId:e.upcoming.waitlistId,updatedAt:Date.now()}:e.recentPast?{type:e.recentPast.type,id:e.recentPast.id,sequenceId:t,lifecycle:"closed",closesAt:e.recentPast.endedAt,updatedAt:Date.now()}:void 0}normalizeOffers(e){return Array.isArray(e)?e.filter(Boolean):[]}restoreFromStore(){const e=this.store.activeJourneys;if(!e||!e[this.experienceId])return null;const t=qe(e[this.experienceId]);return t||(this.store.removeJourney(this.experienceId),null)}persist(){this.store.setJourneyState(this.experienceId,this.stateAtom.get())}setSnapshot(e,t={}){if(this.destroyed)return;const i=this.stateAtom.get();this.stateAtom.set(e),!1!==t.persist&&this.persist(),this.emitPhaseChanged(i,e)}emitPhaseChanged(e,t){const i=e.journeyStage,s="routed"===i?M(e):void 0,r=t.journeyStage,n="routed"===r?M(t):void 0;i===r&&s===n||w().emit("journey:phase-changed",{experienceId:this.experienceId,journeyStage:r,sequencePhase:n})}runSerializedOperation(e){const t=this.operationQueue.then(async()=>{if(this.destroyed)throw c.notInitialized("Journey has been destroyed");return e()});return this.operationQueue=t.then(()=>{},()=>{}),t}transitionToGranted(e,t){if(this.destroyed)return;const i=this.stateAtom.get(),s=_(i),r=G(i);if(!s||!r)return void this.transitionToEnded({type:"closed",reason:"missing_distribution"});if(void 0!==t&&Date.now()>=t)return void this.transitionToEnded({type:"expired"});const n=N({token:e,distribution:s,sequenceId:r,experienceId:this.experienceId,expiresAt:t}),o=this.withGrantedConsumer(z(i),s),a=this.projectSequenceState({distribution:s,consumer:o,grant:n});this.commit(a),this.stopRuntimeMonitoring(!0),this.clearGrantExpiryTimer(),this.clearCapabilityRefreshTimer(),this.checkoutStarted=!1,void 0!==t&&(this.grantExpiryTimer=setTimeout(()=>this.expireGrantFromTimer(),t-Date.now())),this.emitEvent("sequence_change","success","user","Grant issued",{grant:e}),w().emit("journey:granted",{experienceId:this.experienceId,sequenceId:r,grant:e})}transitionToEnded(e){if(this.destroyed)return;const t=this.stateAtom.get(),i=_(t),s=this.reinterpretLapsedGrantOutcome(t,e),r={...s,distributionType:s.distributionType??i?.type,distributionId:s.distributionId??i?.id,at:s.at??/* @__PURE__ */(new Date).toISOString()},n=this.withTerminalConsumer(z(t),r),o=this.projectSequenceState({distribution:i,consumer:n,outcome:r});this.commit(o),this.stopRuntimeMonitoring(!0),this.clearGrantExpiryTimer(),this.clearCapabilityRefreshTimer(),this.emitEvent("sequence_change","expired"===r.type?"warning":"info","user","Sequence ended",{outcome:r}),w().emit("journey:ended",{experienceId:this.experienceId,sequenceId:G(this.stateAtom.get()),outcome:r})}reinterpretLapsedGrantOutcome(e,t){if("granted"!==M(e))return t;return"closed"===t.type&&("not_queued"===t.reason||"not_entered"===t.reason)?{type:"expired",at:t.at,distributionType:t.distributionType,distributionId:t.distributionId}:t}applyMonitorUpdate(e){this.destroyed||("granted"!==e.type?this.transitionToEnded(e.outcome):this.transitionToGranted(e.token,e.expiresAt))}applyMonitorUpdateFromCallback(e){try{this.applyMonitorUpdate(e)}catch(t){if(this.dropStaleCallbackCommit(t,"monitor"))return;throw t}}startMonitorForParticipation(e){const t=this.ensureDisplayAtom(e),i=e=>this.applyMonitorUpdateFromCallback(e),s=this.sequenceDrivers[e.type];if(s?.startMonitoring)return s.startMonitoring({participation:e,displayAtom:t,onUpdate:i}),void(this.activeMonitor={type:e.type,id:e.id,stop:()=>s.stopMonitoring?.()});throw new Error(`Unknown distribution type: ${e.type}`)}stopRuntimeMonitoring(e=!1){this.activeMonitor&&(this.activeMonitor.stop(),this.activeMonitor=null),e&&this.clearRuntimeDisplayState()}clearGrantExpiryTimer(){this.grantExpiryTimer&&(clearTimeout(this.grantExpiryTimer),this.grantExpiryTimer=void 0)}clearCapabilityRefreshTimer(){this.capabilityRefreshTimer&&(clearTimeout(this.capabilityRefreshTimer),this.capabilityRefreshTimer=void 0)}armCapabilityRefreshTimer(){if(this.clearCapabilityRefreshTimer(),this.destroyed||this.checkoutStarted)return;const e=this.stateAtom.get();if("routed"!==e.journeyStage)return;const t=q(this.experienceId,e.sequenceId);if(void 0===t)return;const i=Math.max(t-Date.now()-6e4,5e3);this.scheduleCapabilityRefresh(i)}scheduleCapabilityRefresh(e){if(this.clearCapabilityRefreshTimer(),this.destroyed||this.checkoutStarted)return;"routed"===this.stateAtom.get().journeyStage&&(this.capabilityRefreshTimer=setTimeout(()=>this.refreshCapabilityFromTimer(),e))}rearmTimersIfNeeded(){if(this.destroyed||this.checkoutStarted)return;const e=this.stateAtom.get();if("routed"===e.journeyStage&&this.armCapabilityRefreshTimer(),"granted"!==M(e)||this.grantExpiryTimer)return;const t=V(e);if(!t)return;const i=t-Date.now();i<=0?this.transitionToEnded({type:"expired"}):this.grantExpiryTimer=setTimeout(()=>this.expireGrantFromTimer(),i)}async refreshCapabilityFromTimer(){this.capabilityRefreshTimer=void 0;try{if("routed"!==this.stateAtom.get().journeyStage)return void this.clearCapabilityRefreshTimer();if(this.checkoutStarted)return;const e=Date.now();if(void 0!==this.lastCapabilityRefreshAtMs&&e-this.lastCapabilityRefreshAtMs<H)return void this.scheduleCapabilityRefresh(H);this.lastCapabilityRefreshAtMs=e,await this.retry(),this.consecutiveRefreshFailures=0}catch(e){if(this.dropStaleCallbackCommit(e,"capability refresh timer"))return;const t=Math.min(H*2**this.consecutiveRefreshFailures,6e5);this.consecutiveRefreshFailures+=1,this.emitEvent("error","warning","system","Capability grant refresh failed",{error:e instanceof Error?e.message:String(e),consecutiveFailures:this.consecutiveRefreshFailures}),this.scheduleCapabilityRefresh(t)}}expireGrantFromTimer(){try{this.transitionToEnded({type:"expired"})}catch(e){if(this.dropStaleCallbackCommit(e,"grant-expiry timer"))return;throw e}}dropStaleCallbackCommit(e,t){return!(!d(e)||e.code!==u.VALIDATION_ERROR||e.message!==$&&e.message!==Q)&&(this.logger.debug("Dropped stale journey callback",{experienceId:this.experienceId,source:t,message:e.message}),!0)}getRuntimeBinding(){const e=this.stateAtom.get(),t=M(e),i=k(e),s=G(e);return"participating"===t&&i&&s?{experienceId:this.experienceId,sequenceId:s,participationId:i.id,participationType:i.type}:null}startRuntimeMonitoring(){const e=this.getRuntimeBinding();if(!e)return;const t=this.activeMonitor;t&&t.id===e.participationId&&t.type===e.participationType&&this.displayAtom||(this.stopRuntimeMonitoring(!1),this.startMonitorForParticipation({id:e.participationId,type:e.participationType}))}getRuntimeDisplayAtom(){return this.displayAtom}applyRuntimeState(e){if(!e)return void this.clearRuntimeDisplayState();const t=this.getRuntimeBinding();if(!t)return;this.ensureDisplayAtom({id:t.participationId,type:t.participationType}).set(e)}reconcileSyncedSnapshot(e){if(this.destroyed)return;const t=this.getRuntimeBinding();if(!e)return this.stopRuntimeMonitoring(!0),this.clearGrantExpiryTimer(),this.clearCapabilityRefreshTimer(),void this.setSnapshot(D(this.experienceId),{persist:!1});const i=qe(e);if(!i)return;const s=this.stateAtom.get();if(s.revision===i.revision&&s.updatedAt===i.updatedAt)return;const r=k(i),n=M(i),o="participating"===n&&r?{sequenceId:G(i),participationId:r.id,participationType:r.type}:null;o?t&&t.sequenceId===o.sequenceId&&t.participationId===o.participationId&&t.participationType===o.participationType||this.resetDisplayAtom(o.participationType):this.displayAtom=null,this.setSnapshot(i,{persist:!1}),"participating"===n&&r?this.ensureDisplayAtom(r):this.stopRuntimeMonitoring(!0),this.clearGrantExpiryTimer(),this.clearCapabilityRefreshTimer(),this.rearmTimersIfNeeded()}ensureDisplayAtom(e){return this.resetDisplayAtom(e.type),this.displayAtom}resetDisplayAtom(t){this.displayAtom&&this.displayAtom.get().type===t?this.displayAtom.set({type:t}):this.displayAtom=e({type:t})}clearRuntimeDisplayState(){const e=this.getRuntimeBinding();e?this.resetDisplayAtom(e.participationType):this.displayAtom=null}clearRoutedRuntimeState(){this.stopRuntimeMonitoring(!0),this.clearGrantExpiryTimer(),this.clearCapabilityRefreshTimer(),this.checkoutStarted=!1}deriveTerminalOutcomeForClosedDistribution(e,t,i){if(i)return i;if(!t||"closed"!==t.lifecycle||!e)return;const s=this.projectSequenceState({distribution:t,consumer:e});return"ended"===s.phase&&"lost"===s.outcome.type?s.outcome:{type:"closed",distributionType:t.type,distributionId:t.id}}withGrantedConsumer(e,t){switch(e?.mechanism){case"auction":return o(e,t);case"draw":return m(e,t);case"queue":return f(e,t);case"timed_release":return R(e,t);case void 0:switch(t.type){case"auction":return o(e,t);case"draw":return m(e,t);case"queue":return f(e,t);case"timed_release":return R(e,t);default:throw c.validationError("Distribution does not produce grants")}default:if(!e)throw c.validationError("Grant requires a consumer state");return e}}withTerminalConsumer(e,t){switch(e?.mechanism){case"appointment":return r(e,t);case"auction":return a(e,t);case"draw":return y(e,t);case"queue":return b(e,t);case"timed_release":return j(e,t);default:return e}}stopPolling(){this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=void 0)}}function X(e,t){if(e&&e.waitlistId===t.waitlistId)return e}const Y=i.picklist(["queue","draw","auction","timed_release","appointment"]),Z=Y,ee=i.picklist(["queue","draw","auction","timed_release"]),te=i.picklist(["draw","auction"]),ie=i.strictObject({id:i.string(),ts:i.number(),kind:i.picklist(["reroute","sequence_change","distribution_change","requirement","error","info"]),severity:i.picklist(["info","success","warning","error"]),audience:i.picklist(["user","system","analytics"]),message:i.string(),detail:i.optional(i.record(i.string(),i.unknown()))}),se=i.strictObject({id:i.string(),name:i.string(),experienceId:i.string(),priority:i.number(),color:i.string()}),re=i.strictObject({type:Y,id:i.string(),sequenceId:i.optional(i.string()),lifecycle:i.string(),startsAt:i.optional(i.string()),opensAt:i.optional(i.string()),closesAt:i.optional(i.string()),settlesAt:i.optional(i.string()),waitlistId:i.optional(i.string()),details:i.optional(i.unknown()),updatedAt:i.optional(i.number())}),ne=i.strictObject({status:i.string(),id:i.optional(i.string()),distributionId:i.optional(i.string()),waitlistId:i.optional(i.string()),enteredAt:i.optional(i.string()),admissionGrant:i.optional(i.string()),grantedAt:i.optional(i.string()),deniedReason:i.optional(i.string()),position:i.optional(i.number()),highestBid:i.optional(i.string()),currentBid:i.optional(i.string()),slotId:i.optional(i.string()),locationId:i.optional(i.string()),bookedAt:i.optional(i.string()),checkedInAt:i.optional(i.string()),completedAt:i.optional(i.string()),cancelledAt:i.optional(i.string()),noShowAt:i.optional(i.string()),notifiedAt:i.optional(i.string())}),oe=i.strictObject({token:i.string(),distributionType:ee,distributionId:i.string(),sequenceId:i.string(),experienceId:i.string(),issuedAt:i.optional(i.string()),expiresAt:i.optional(i.string()),handoffUrl:i.optional(i.string())}),ae=i.strictObject({type:i.string(),reason:i.optional(i.string()),at:i.optional(i.string()),distributionType:i.optional(Y),distributionId:i.optional(i.string())}),ce=i.strictObject({phase:i.literal("unavailable"),reason:i.optional(i.string())}),de=i.strictObject({waitlistId:i.string(),status:i.picklist(["not_waitlisted","waitlisted","notified"]),entryId:i.optional(i.string())}),ue=i.strictObject({phase:i.literal("scheduled"),mechanism:Z,distribution:re,waitlist:i.optional(de)}),he=i.strictObject({phase:i.literal("enterable"),mechanism:Z,distribution:re,consumer:ne}),pe=i.strictObject({phase:i.literal("participating"),mechanism:Z,distribution:re,consumer:ne}),le=i.strictObject({phase:i.literal("settling"),mechanism:te,distribution:re,consumer:ne}),me=i.strictObject({phase:i.literal("granted"),mechanism:ee,distribution:re,consumer:ne,grant:oe}),ye=i.strictObject({phase:i.literal("ended"),mechanism:Z,distribution:re,consumer:ne,outcome:ae}),ge=i.union([ce,ue,he,pe,le,me,ye]),fe={schemaVersion:i.literal(2),revision:i.number(),updatedAt:i.number(),experienceId:i.string(),accessCode:i.optional(i.string()),experience:i.optional(i.unknown()),events:i.array(ie),lastSeenEventId:i.optional(i.string())},be=i.strictObject({...fe,journeyStage:i.literal("ready")}),Ie=i.strictObject({...fe,journeyStage:i.literal("routing")}),ve=i.strictObject({...fe,journeyStage:i.literal("gated"),gates:i.array(i.unknown()),selected:i.optional(i.strictObject({sequence:i.unknown(),status:i.literal("gated")}))}),Se=i.strictObject({...fe,journeyStage:i.literal("routed"),sequenceId:i.string(),offers:i.array(i.unknown()),sequence:ge,sequenceRef:i.optional(se)}),Ae=i.union([be,Ie,ve,Se]);function qe(e){const t=i.safeParse(Ae,e);if(!t.success)return null;const{schemaVersion:s,...r}=t.output;if("routed"===r.journeyStage)try{W(r.sequence)}catch{return null}return r}export{K as ExperienceJourney};
@@ -1,4 +1,4 @@
1
- import { AdmissionGrant, DistributionSummary, GatedSnapshot, JourneySnapshot, JourneyStage, MechanismConsumerState, Participation, ReadySnapshot, RoutedSnapshot, RoutingGate, RoutingOffer, SequenceMechanism, SequenceOutcome, SequenceState, SequenceTargetState } from './journey.types';
1
+ import { AdmissionGrant, DistributionSummary, GatedSnapshot, JourneySnapshot, JourneyStage, MechanismConsumerState, Participation, ReadySnapshot, RoutedSnapshot, RoutingGate, RoutingOffer, SequenceMechanism, SequenceOutcome, SequenceState, WaitlistAttachment } from './journey.types';
2
2
  import { ExperienceSession, Sequence } from './types';
3
3
  export declare class SnapshotInvariantError extends Error {
4
4
  constructor(phase: string, missing: string);
@@ -8,7 +8,7 @@ export declare function isRoutedState(state: JourneySnapshot): state is RoutedSn
8
8
  export declare function getGates(state: JourneySnapshot): RoutingGate[];
9
9
  export declare function getSequenceId(state: JourneySnapshot): string | undefined;
10
10
  export declare function getDistribution(state: JourneySnapshot): DistributionSummary | undefined;
11
- export declare function getTarget(state: JourneySnapshot): SequenceTargetState | undefined;
11
+ export declare function getWaitlistAttachment(state: JourneySnapshot): WaitlistAttachment | undefined;
12
12
  export declare function getConsumer(state: JourneySnapshot): MechanismConsumerState | undefined;
13
13
  export declare function getParticipation(state: JourneySnapshot): Participation | undefined;
14
14
  export declare function getAdmissionGrant(state: JourneySnapshot): string | undefined;
@@ -41,11 +41,7 @@ export declare function buildSnapshot(params: {
41
41
  sequenceId?: string;
42
42
  offers?: RoutingOffer[];
43
43
  sequence?: SequenceState;
44
- distribution?: DistributionSummary;
45
- target?: SequenceTargetState;
46
- consumer?: MechanismConsumerState;
47
- grant?: AdmissionGrant;
48
- outcome?: SequenceOutcome;
44
+ sequenceRef?: Sequence;
49
45
  }): JourneySnapshot;
50
46
  export declare function validateSequenceState(data: SequenceState): SequenceState;
51
47
  export declare function createGrant(params: {
@@ -55,23 +51,10 @@ export declare function createGrant(params: {
55
51
  experienceId: string;
56
52
  expiresAt?: number;
57
53
  }): AdmissionGrant;
58
- export declare function deriveSequenceState(params: {
59
- distribution?: DistributionSummary;
60
- target?: SequenceTargetState;
61
- consumer?: MechanismConsumerState;
62
- grant?: AdmissionGrant;
63
- outcome?: SequenceOutcome;
64
- }): SequenceState;
65
54
  export declare function normalizeSequenceState(params: {
66
55
  sequence: SequenceState;
67
- distribution?: DistributionSummary;
68
- target?: SequenceTargetState;
69
- consumer?: MechanismConsumerState;
70
- grant?: AdmissionGrant;
71
- outcome?: SequenceOutcome;
72
56
  }): SequenceState;
73
57
  export declare function sequenceDistribution(sequence: SequenceState): DistributionSummary | undefined;
74
- export declare function sequenceTarget(sequence: SequenceState): SequenceTargetState | undefined;
75
58
  export declare function withMechanism(mechanism: SequenceMechanism, consumer: unknown): MechanismConsumerState;
76
59
  export declare function stripMechanism<T extends {
77
60
  mechanism: SequenceMechanism;
@@ -1 +1 @@
1
- class e extends Error{constructor(e,t){super(`SnapshotInvariantError: phase '${e}' requires ${t}`),this.name="SnapshotInvariantError"}}function t(e){return"gated"===e.journeyStage}function n(e){return"routed"===e.journeyStage}function i(e){return t(e)?e.gates:[]}function r(e){return n(e)?e.sequenceId:void 0}function s(e){if(!n(e))return;const t=e.sequence;return"distribution"in t?t.distribution:void 0}function o(e){if(!n(e))return;const t=e.sequence;return"target"in t?t.target:void 0}function u(e){if(!n(e))return;const t=e.sequence;return"mechanism"in t?S(t.mechanism,t.consumer):void 0}function a(e){if(!n(e))return;if("participating"!==e.sequence.phase)return;const t=e.sequence,i=t.consumer,r="waitlist"===t.mechanism?D(i.waitlistId)??D(i.id)??o(e)?.waitlistId:D(i.distributionId)??D(i.id)??s(e)?.id;return r?{id:r,type:t.mechanism}:void 0}function c(e){return d(e)?.token}function d(e){return n(e)&&"granted"===e.sequence.phase?e.sequence.grant:void 0}function m(e){const t=d(e);if(t?.expiresAt)return new Date(t.expiresAt).getTime()}function h(e){return n(e)&&"ended"===e.sequence.phase?e.sequence.outcome:void 0}function p(e){return t(e)?e.selected:void 0}function l(e){return n(e)?e.offers:[]}function w(e){return n(e)?e.sequence.phase:"unavailable"}function f(e){return n(e)?e.sequence:void 0}function g(e){return{revision:1,updatedAt:Date.now(),journeyStage:"ready",experienceId:e,events:[],lastSeenEventId:void 0}}function b(t){const n=function(e){return{...e,revision:e.revision+1,updatedAt:Date.now()}}(t.base);switch(t.journeyStage){case"ready":return{...n,journeyStage:"ready"};case"routing":return{...n,journeyStage:"routing"};case"gated":return{...n,journeyStage:"gated",gates:t.gates??[],selected:t.selected};case"routed":{if(!t.sequenceId)throw new e("routed","sequenceId");const i=t.sequence??q({distribution:t.distribution,target:t.target,consumer:t.consumer,grant:t.grant,outcome:t.outcome});return y(i),{...n,journeyStage:"routed",sequenceId:t.sequenceId,offers:t.offers??[],sequence:i}}}throw new e(String(t.journeyStage),"known journeyStage")}function y(t){const n=t;switch(t.phase){case"unavailable":default:return t;case"scheduled":if(!("mechanism"in n))throw new e("scheduled","mechanism");if("waitlist"===n.mechanism)throw new e("scheduled","primary mechanism");if(!("distribution"in n))throw new e("scheduled","distribution");if(!("consumer"in n))throw new e("scheduled","consumer");return t;case"enterable":case"participating":if(!("mechanism"in n))throw new e(t.phase,"mechanism");if("waitlist"===n.mechanism){if(!("target"in n))throw new e(t.phase,"target")}else if(!("distribution"in n))throw new e(t.phase,"distribution");if(!("consumer"in n))throw new e(t.phase,"consumer");return t;case"settling":if(!("mechanism"in n))throw new e("settling","mechanism");if("draw"!==t.mechanism&&"auction"!==t.mechanism)throw new e("settling","draw or auction mechanism");if(!("distribution"in n))throw new e("settling","distribution");if(!("consumer"in n))throw new e("settling","consumer");return t;case"granted":if(!("mechanism"in n))throw new e("granted","mechanism");if("appointment"===n.mechanism||"waitlist"===n.mechanism)throw new e("granted","grant-producing mechanism");if(!("distribution"in n))throw new e("granted","distribution");if(!("consumer"in n))throw new e("granted","consumer");if(!("grant"in n))throw new e("granted","grant");return t;case"ended":if(!("mechanism"in n))throw new e("ended","mechanism");if("waitlist"===n.mechanism){if(!("target"in n))throw new e("ended","target")}else if(!("distribution"in n))throw new e("ended","distribution");if(!("consumer"in n))throw new e("ended","consumer");if(!t.outcome)throw new e("ended","outcome");return t}}function I(t){if("appointment"===t.distribution.type)throw new e("granted","grant-producing mechanism");return{token:t.token,distributionType:t.distribution.type,distributionId:t.distribution.id,sequenceId:t.sequenceId,experienceId:t.experienceId,expiresAt:t.expiresAt?new Date(t.expiresAt).toISOString():void 0}}function q(e){const{distribution:t,target:n,consumer:i,grant:r,outcome:s}=e;if("waitlist"===i?.mechanism||n){const e=n??A(t);if(!e)return{phase:"unavailable",reason:"missing_waitlist"};const r="waitlist"===i?.mechanism?x(i):{status:"not_waitlisted"},o=s??k("waitlist",r);return o?{phase:"ended",mechanism:"waitlist",target:e,consumer:r,outcome:o}:{phase:"not_waitlisted"===r.status?"enterable":"participating",mechanism:"waitlist",target:e,consumer:r}}if(!t)return{phase:"unavailable"};const o=t.type,u=i&&i.mechanism===o?x(i):j(t);if(r)return r.expiresAt&&Date.now()>new Date(r.expiresAt).getTime()?"open"===t.lifecycle?{phase:"enterable",mechanism:o,distribution:t,consumer:j(t)}:_(o,t,u,{type:"expired"}):{phase:"granted",mechanism:o,distribution:t,consumer:u,grant:r};const a=s??k(o,u);if(a)return _(o,t,u,a);if(function(e,t){const n=t.status;switch(e){case"queue":return"queued"===n;case"draw":case"timed_release":return"entered"===n;case"auction":return"bidding"===n||"winning"===n||"outbid"===n;case"appointment":return"booked"===n||"checked_in"===n}}(o,u))return{phase:"participating",mechanism:o,distribution:t,consumer:u};if("scheduled"===t.lifecycle&&t.waitlistId){const e=A(t);if(e)return{phase:"enterable",mechanism:"waitlist",target:e,consumer:{status:"not_waitlisted"}}}return"scheduled"===t.lifecycle?{phase:"scheduled",mechanism:o,distribution:t,consumer:u}:"open"===t.lifecycle?{phase:"enterable",mechanism:o,distribution:t,consumer:u}:"settling"!==t.lifecycle||"draw"!==o&&"auction"!==o?_(o,t,u,{type:"closed"}):{phase:"settling",mechanism:o,distribution:t,consumer:u}}function v(e){try{return y(e.sequence)}catch{return q({distribution:e.distribution,target:e.target,consumer:e.consumer,grant:e.grant,outcome:e.outcome})}}function S(e,t){return{...t,mechanism:e}}function x(e){const{mechanism:t,...n}=e;return n}function _(e,t,n,i){return{phase:"ended",mechanism:e,distribution:t,consumer:n,outcome:{...i,distributionType:i.distributionType??t.type,distributionId:i.distributionId??t.id,at:i.at??/* @__PURE__ */(new Date).toISOString()}}}function A(e){if(e?.waitlistId)return{type:e.type,id:e.id,sequenceId:e.sequenceId,lifecycle:e.lifecycle,opensAt:e.opensAt,closesAt:e.closesAt,waitlistId:e.waitlistId}}function j(e){switch(e.type){case"queue":return{status:"not_queued",distributionId:e.id};case"draw":case"timed_release":return{status:"not_entered",distributionId:e.id};case"auction":return{status:"not_bid",distributionId:e.id};case"appointment":return{status:"not_booked",distributionId:e.id}}}function k(e,t){const n=t.status,i=t.deniedReason;switch(e){case"queue":case"timed_release":return"completed"===n?{type:"completed"}:"denied"===n?{type:"denied",reason:i}:"left"===n?{type:"left"}:void 0;case"draw":return"completed"===n?{type:"completed"}:"denied"===n?{type:"denied",reason:i}:void 0;case"auction":return"lost"===n?{type:"lost"}:void 0;case"appointment":return"completed"===n?{type:"completed"}:"cancelled"===n?{type:"cancelled"}:"no_show"===n?{type:"no_show"}:void 0;case"waitlist":return"expired"===n?{type:"expired"}:"left"===n?{type:"left"}:void 0}}function D(e){return"string"==typeof e&&e.length>0?e:void 0}export{e as SnapshotInvariantError,b as buildSnapshot,I as createGrant,g as createInitialSnapshot,q as deriveSequenceState,c as getAdmissionGrant,m as getAdmissionGrantExpiresAt,u as getConsumer,s as getDistribution,i as getGates,d as getGrant,l as getOffers,h as getOutcome,a as getParticipation,p as getSelected,r as getSequenceId,w as getSequencePhase,f as getSequenceState,o as getTarget,t as isGateState,n as isRoutedState,v as normalizeSequenceState,x as stripMechanism,y as validateSequenceState,S as withMechanism};
1
+ class e extends Error{constructor(e,n){super(`SnapshotInvariantError: phase '${e}' requires ${n}`),this.name="SnapshotInvariantError"}}function n(e){return"gated"===e.journeyStage}function t(e){return"routed"===e.journeyStage}function i(e){return n(e)?e.gates:[]}function r(e){return t(e)?e.sequenceId:void 0}function u(e){if(!t(e))return;const n=e.sequence;return"distribution"in n?n.distribution:void 0}function o(e){if(!t(e))return;const n=e.sequence;return"waitlist"in n?n.waitlist:void 0}function s(e){if(!t(e))return;const n=e.sequence;return"mechanism"in n&&"consumer"in n?I(n.mechanism,n.consumer):void 0}function c(e){if(!t(e))return;if("participating"!==e.sequence.phase)return;const n=e.sequence,i=n.consumer,r=S(i.distributionId)??S(i.id)??u(e)?.id;return r?{id:r,type:n.mechanism}:void 0}function a(e){return d(e)?.token}function d(e){return t(e)&&"granted"===e.sequence.phase?e.sequence.grant:void 0}function f(e){const n=d(e);if(n?.expiresAt)return new Date(n.expiresAt).getTime()}function h(e){return t(e)&&"ended"===e.sequence.phase?e.sequence.outcome:void 0}function w(e){return n(e)?e.selected:void 0}function m(e){return t(e)?e.offers:[]}function g(e){return t(e)?e.sequence.phase:"unavailable"}function p(e){return t(e)?e.sequence:void 0}function l(e){return{revision:1,updatedAt:Date.now(),journeyStage:"ready",experienceId:e,events:[],lastSeenEventId:void 0}}function q(n){const t=function(e){return{...e,revision:e.revision+1,updatedAt:Date.now()}}(n.base);switch(n.journeyStage){case"ready":return{...t,journeyStage:"ready"};case"routing":return{...t,journeyStage:"routing"};case"gated":return{...t,journeyStage:"gated",gates:n.gates??[],selected:n.selected};case"routed":{if(!n.sequenceId)throw new e("routed","sequenceId");if(!n.sequence)throw new e("routed","sequence");const i=n.sequence;return b(i),{...t,journeyStage:"routed",sequenceId:n.sequenceId,offers:n.offers??[],sequence:i,sequenceRef:n.sequenceRef}}}throw new e(String(n.journeyStage),"known journeyStage")}function b(n){const t=n;switch(n.phase){case"unavailable":default:return n;case"scheduled":if(!("mechanism"in t))throw new e("scheduled","mechanism");if(!("distribution"in t))throw new e("scheduled","distribution");if("consumer"in t)throw new e("scheduled","no consumer");return n;case"enterable":case"participating":if(!("mechanism"in t))throw new e(n.phase,"mechanism");if(!("distribution"in t))throw new e(n.phase,"distribution");if(!("consumer"in t))throw new e(n.phase,"consumer");if("waitlist"in t)throw new e(n.phase,"no waitlist attachment");return n;case"settling":if(!("mechanism"in t))throw new e("settling","mechanism");if(!("distribution"in t))throw new e("settling","distribution");if(!("consumer"in t))throw new e("settling","consumer");return n;case"granted":if(!("mechanism"in t))throw new e("granted","mechanism");if(!("distribution"in t))throw new e("granted","distribution");if(!("consumer"in t))throw new e("granted","consumer");if(!("grant"in t))throw new e("granted","grant");return n;case"ended":if(!("mechanism"in t))throw new e("ended","mechanism");if(!("distribution"in t))throw new e("ended","distribution");if(!("consumer"in t))throw new e("ended","consumer");if(!n.outcome)throw new e("ended","outcome");return n}}function v(n){if("appointment"===n.distribution.type)throw new e("granted","grant-producing mechanism");return{token:n.token,distributionType:n.distribution.type,distributionId:n.distribution.id,sequenceId:n.sequenceId,experienceId:n.experienceId,expiresAt:n.expiresAt?new Date(n.expiresAt).toISOString():void 0}}function y(e){try{return b(e.sequence)}catch{return{phase:"unavailable"}}}function I(e,n){return{...n,mechanism:e}}function S(e){return"string"==typeof e&&e.length>0?e:void 0}export{e as SnapshotInvariantError,q as buildSnapshot,v as createGrant,l as createInitialSnapshot,a as getAdmissionGrant,f as getAdmissionGrantExpiresAt,s as getConsumer,u as getDistribution,i as getGates,d as getGrant,m as getOffers,h as getOutcome,c as getParticipation,w as getSelected,r as getSequenceId,g as getSequencePhase,p as getSequenceState,o as getWaitlistAttachment,n as isGateState,t as isRoutedState,y as normalizeSequenceState,b as validateSequenceState,I as withMechanism};