gapless.js 4.0.2 → 4.0.4

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.
@@ -8,6 +8,12 @@
8
8
  // ended The last track in the queue has finished.
9
9
  //
10
10
  // Root-level `on:` eliminates the handler duplication that plagued v2.
11
+ //
12
+ // Named actions (no-op defaults here, real implementations via .provide()):
13
+ // Actions before assign() see the OLD context (e.g. deactivateCurrent
14
+ // sees the old currentTrackIndex). Actions after assign() see the NEW
15
+ // context (e.g. activateAndPlayCurrent sees the incremented index).
16
+ // Implementations MUST use the ({ context }) parameter, NOT getSnapshot().
11
17
  // ---------------------------------------------------------------------------
12
18
 
13
19
  import { setup, assign } from 'xstate';
@@ -48,6 +54,60 @@ export function createQueueMachine(initialContext: QueueContext) {
48
54
  playImmediately: ({ event }) =>
49
55
  !!(event as { type: 'GOTO'; playImmediately?: boolean }).playImmediately,
50
56
  },
57
+ actions: {
58
+ // --- named assign actions (avoids mixing inline assign with custom actions) ---
59
+ incrementTrackCount: assign({
60
+ trackCount: ({ context }) => context.trackCount + 1,
61
+ }),
62
+ decrementTrackCount: assign({
63
+ trackCount: ({ context }) => Math.max(0, context.trackCount - 1),
64
+ currentTrackIndex: ({ context, event }) => {
65
+ const e = event as { type: 'REMOVE_TRACK'; index: number };
66
+ if (e.index < context.currentTrackIndex) {
67
+ return Math.max(0, context.currentTrackIndex - 1);
68
+ }
69
+ return context.currentTrackIndex;
70
+ },
71
+ }),
72
+ gotoTrackIndex: assign({
73
+ currentTrackIndex: ({ event }) =>
74
+ (event as { type: 'GOTO'; index: number }).index,
75
+ }),
76
+ advanceToNextTrack: assign({
77
+ currentTrackIndex: ({ context }) => {
78
+ const next = context.currentTrackIndex + 1;
79
+ return next < context.trackCount ? next : context.currentTrackIndex;
80
+ },
81
+ }),
82
+ goToPreviousTrack: assign({
83
+ currentTrackIndex: ({ context }) => Math.max(0, context.currentTrackIndex - 1),
84
+ }),
85
+ advanceOnTrackEnd: assign({
86
+ currentTrackIndex: ({ context }) => context.currentTrackIndex + 1,
87
+ }),
88
+ resetToFirstTrack: assign({
89
+ currentTrackIndex: () => 0,
90
+ }),
91
+ // --- side-effect actions (no-op defaults, provided by Queue.ts) ---
92
+ deactivateCurrent: () => {},
93
+ deactivateEndedTrack: () => {},
94
+ activateAndPlayCurrent: () => {},
95
+ playOrContinueGapless: () => {},
96
+ cancelAllGapless: () => {},
97
+ notifyStartNewTrack: () => {},
98
+ notifyPlayNextTrack: () => {},
99
+ notifyPlayPreviousTrack: () => {},
100
+ notifyEnded: () => {},
101
+ updateMediaSessionMetadata: () => {},
102
+ preloadAhead: () => {},
103
+ playCurrent: () => {},
104
+ pauseCurrent: () => {},
105
+ seekCurrent: () => {},
106
+ seekCurrentToZero: () => {},
107
+ scheduleGapless: () => {},
108
+ cancelScheduledGapless: () => {},
109
+ cancelAndRescheduleGapless: () => {},
110
+ },
51
111
  }).createMachine({
52
112
  id: 'queue',
53
113
  initial: 'idle',
@@ -56,19 +116,10 @@ export function createQueueMachine(initialContext: QueueContext) {
56
116
  // Global handlers — shared across all states
57
117
  on: {
58
118
  ADD_TRACK: {
59
- actions: assign({ trackCount: ({ context }) => context.trackCount + 1 }),
119
+ actions: 'incrementTrackCount',
60
120
  },
61
121
  REMOVE_TRACK: {
62
- actions: assign({
63
- trackCount: ({ context }) => Math.max(0, context.trackCount - 1),
64
- currentTrackIndex: ({ context, event }) => {
65
- const e = event as { type: 'REMOVE_TRACK'; index: number };
66
- if (e.index < context.currentTrackIndex) {
67
- return Math.max(0, context.currentTrackIndex - 1);
68
- }
69
- return context.currentTrackIndex;
70
- },
71
- }),
122
+ actions: 'decrementTrackCount',
72
123
  },
73
124
  },
74
125
 
@@ -78,22 +129,38 @@ export function createQueueMachine(initialContext: QueueContext) {
78
129
  // -----------------------------------------------------------------
79
130
  idle: {
80
131
  on: {
81
- PLAY: { target: 'playing' },
132
+ PLAY: {
133
+ target: 'playing',
134
+ actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],
135
+ },
82
136
  GOTO: [
83
137
  {
84
138
  guard: 'playImmediately',
85
139
  target: 'playing',
86
- actions: assign({
87
- currentTrackIndex: ({ event }) => (event as { type: 'GOTO'; index: number }).index,
88
- }),
140
+ actions: [
141
+ 'deactivateCurrent',
142
+ 'cancelAllGapless',
143
+ 'gotoTrackIndex',
144
+ 'activateAndPlayCurrent',
145
+ 'notifyStartNewTrack',
146
+ 'updateMediaSessionMetadata',
147
+ 'preloadAhead',
148
+ ],
89
149
  },
90
150
  {
91
151
  target: 'paused',
92
- actions: assign({
93
- currentTrackIndex: ({ event }) => (event as { type: 'GOTO'; index: number }).index,
94
- }),
152
+ actions: [
153
+ 'deactivateCurrent',
154
+ 'cancelAllGapless',
155
+ 'gotoTrackIndex',
156
+ 'seekCurrentToZero',
157
+ 'preloadAhead',
158
+ ],
95
159
  },
96
160
  ],
161
+ TRACK_LOADED: {
162
+ actions: ['preloadAhead'],
163
+ },
97
164
  },
98
165
  },
99
166
 
@@ -102,38 +169,86 @@ export function createQueueMachine(initialContext: QueueContext) {
102
169
  // -----------------------------------------------------------------
103
170
  playing: {
104
171
  on: {
105
- PAUSE: { target: 'paused' },
106
- TOGGLE: { target: 'paused' },
172
+ PAUSE: {
173
+ target: 'paused',
174
+ actions: ['cancelScheduledGapless', 'pauseCurrent'],
175
+ },
176
+ TOGGLE: {
177
+ target: 'paused',
178
+ actions: ['cancelScheduledGapless', 'pauseCurrent'],
179
+ },
107
180
  NEXT: {
108
- actions: assign({
109
- currentTrackIndex: ({ context }) => {
110
- const next = context.currentTrackIndex + 1;
111
- return next < context.trackCount ? next : context.currentTrackIndex;
112
- },
113
- }),
181
+ actions: [
182
+ 'deactivateCurrent',
183
+ 'cancelAllGapless',
184
+ 'advanceToNextTrack',
185
+ 'activateAndPlayCurrent',
186
+ 'notifyStartNewTrack',
187
+ 'notifyPlayNextTrack',
188
+ 'updateMediaSessionMetadata',
189
+ 'preloadAhead',
190
+ ],
114
191
  },
115
192
  PREVIOUS: {
116
- actions: assign({
117
- currentTrackIndex: ({ context }) => Math.max(0, context.currentTrackIndex - 1),
118
- }),
193
+ actions: [
194
+ 'deactivateCurrent',
195
+ 'cancelAllGapless',
196
+ 'goToPreviousTrack',
197
+ 'activateAndPlayCurrent',
198
+ 'notifyStartNewTrack',
199
+ 'notifyPlayPreviousTrack',
200
+ 'updateMediaSessionMetadata',
201
+ 'preloadAhead',
202
+ ],
119
203
  },
120
- GOTO: {
121
- actions: assign({
122
- currentTrackIndex: ({ event }) => (event as { type: 'GOTO'; index: number }).index,
123
- }),
204
+ GOTO: [
205
+ {
206
+ guard: 'playImmediately',
207
+ actions: [
208
+ 'deactivateCurrent',
209
+ 'cancelAllGapless',
210
+ 'gotoTrackIndex',
211
+ 'activateAndPlayCurrent',
212
+ 'notifyStartNewTrack',
213
+ 'updateMediaSessionMetadata',
214
+ 'preloadAhead',
215
+ ],
216
+ },
217
+ {
218
+ actions: [
219
+ 'deactivateCurrent',
220
+ 'cancelAllGapless',
221
+ 'gotoTrackIndex',
222
+ 'seekCurrentToZero',
223
+ 'preloadAhead',
224
+ ],
225
+ },
226
+ ],
227
+ SEEK: {
228
+ actions: ['seekCurrent', 'cancelAndRescheduleGapless'],
124
229
  },
125
- SEEK: {},
126
230
  TRACK_ENDED: [
127
231
  {
128
232
  guard: 'hasNextTrack',
129
233
  target: 'playing',
130
- actions: assign({
131
- currentTrackIndex: ({ context }) => context.currentTrackIndex + 1,
132
- }),
234
+ actions: [
235
+ 'deactivateEndedTrack',
236
+ 'advanceOnTrackEnd',
237
+ 'playOrContinueGapless',
238
+ 'notifyStartNewTrack',
239
+ 'notifyPlayNextTrack',
240
+ 'updateMediaSessionMetadata',
241
+ 'preloadAhead',
242
+ ],
243
+ },
244
+ {
245
+ target: 'ended',
246
+ actions: ['deactivateEndedTrack', 'notifyEnded'],
133
247
  },
134
- { target: 'ended' },
135
248
  ],
136
- TRACK_LOADED: {},
249
+ TRACK_LOADED: {
250
+ actions: ['scheduleGapless', 'preloadAhead'],
251
+ },
137
252
  },
138
253
  },
139
254
 
@@ -142,46 +257,84 @@ export function createQueueMachine(initialContext: QueueContext) {
142
257
  // -----------------------------------------------------------------
143
258
  paused: {
144
259
  on: {
145
- PLAY: { target: 'playing' },
146
- TOGGLE: { target: 'playing' },
260
+ PLAY: {
261
+ target: 'playing',
262
+ actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],
263
+ },
264
+ TOGGLE: {
265
+ target: 'playing',
266
+ actions: ['playCurrent', 'updateMediaSessionMetadata', 'preloadAhead', 'scheduleGapless'],
267
+ },
147
268
  NEXT: {
148
- actions: assign({
149
- currentTrackIndex: ({ context }) => {
150
- const next = context.currentTrackIndex + 1;
151
- return next < context.trackCount ? next : context.currentTrackIndex;
152
- },
153
- }),
269
+ actions: [
270
+ 'deactivateCurrent',
271
+ 'cancelAllGapless',
272
+ 'advanceToNextTrack',
273
+ 'activateAndPlayCurrent',
274
+ 'notifyStartNewTrack',
275
+ 'notifyPlayNextTrack',
276
+ 'updateMediaSessionMetadata',
277
+ 'preloadAhead',
278
+ ],
154
279
  },
155
280
  PREVIOUS: {
156
- actions: assign({
157
- currentTrackIndex: ({ context }) => Math.max(0, context.currentTrackIndex - 1),
158
- }),
281
+ actions: [
282
+ 'deactivateCurrent',
283
+ 'cancelAllGapless',
284
+ 'goToPreviousTrack',
285
+ 'activateAndPlayCurrent',
286
+ 'notifyStartNewTrack',
287
+ 'notifyPlayPreviousTrack',
288
+ 'updateMediaSessionMetadata',
289
+ 'preloadAhead',
290
+ ],
159
291
  },
160
292
  GOTO: [
161
293
  {
162
294
  guard: 'playImmediately',
163
295
  target: 'playing',
164
- actions: assign({
165
- currentTrackIndex: ({ event }) => (event as { type: 'GOTO'; index: number }).index,
166
- }),
296
+ actions: [
297
+ 'deactivateCurrent',
298
+ 'cancelAllGapless',
299
+ 'gotoTrackIndex',
300
+ 'activateAndPlayCurrent',
301
+ 'notifyStartNewTrack',
302
+ 'updateMediaSessionMetadata',
303
+ 'preloadAhead',
304
+ ],
167
305
  },
168
306
  {
169
- actions: assign({
170
- currentTrackIndex: ({ event }) => (event as { type: 'GOTO'; index: number }).index,
171
- }),
307
+ actions: [
308
+ 'deactivateCurrent',
309
+ 'cancelAllGapless',
310
+ 'gotoTrackIndex',
311
+ 'seekCurrentToZero',
312
+ 'preloadAhead',
313
+ ],
172
314
  },
173
315
  ],
174
- SEEK: {},
316
+ SEEK: {
317
+ actions: ['seekCurrent'],
318
+ },
175
319
  TRACK_ENDED: [
176
320
  {
177
321
  guard: 'hasNextTrack',
178
322
  target: 'paused',
179
- actions: assign({
180
- currentTrackIndex: ({ context }) => context.currentTrackIndex + 1,
181
- }),
323
+ actions: [
324
+ 'deactivateEndedTrack',
325
+ 'advanceOnTrackEnd',
326
+ 'notifyStartNewTrack',
327
+ 'updateMediaSessionMetadata',
328
+ ],
329
+ },
330
+ {
331
+ target: 'ended',
332
+ actions: ['deactivateEndedTrack', 'notifyEnded'],
182
333
  },
183
- { target: 'ended' },
184
334
  ],
335
+ TRACK_LOADED: {
336
+ actions: ['preloadAhead'],
337
+ },
185
338
  },
186
339
  },
187
340
 
@@ -192,21 +345,37 @@ export function createQueueMachine(initialContext: QueueContext) {
192
345
  on: {
193
346
  PLAY: {
194
347
  target: 'playing',
195
- actions: assign({ currentTrackIndex: () => 0 }),
348
+ actions: [
349
+ 'resetToFirstTrack',
350
+ 'playCurrent',
351
+ 'updateMediaSessionMetadata',
352
+ 'preloadAhead',
353
+ 'scheduleGapless',
354
+ ],
196
355
  },
197
356
  GOTO: [
198
357
  {
199
358
  guard: 'playImmediately',
200
359
  target: 'playing',
201
- actions: assign({
202
- currentTrackIndex: ({ event }) => (event as { type: 'GOTO'; index: number }).index,
203
- }),
360
+ actions: [
361
+ 'deactivateCurrent',
362
+ 'cancelAllGapless',
363
+ 'gotoTrackIndex',
364
+ 'activateAndPlayCurrent',
365
+ 'notifyStartNewTrack',
366
+ 'updateMediaSessionMetadata',
367
+ 'preloadAhead',
368
+ ],
204
369
  },
205
370
  {
206
371
  target: 'paused',
207
- actions: assign({
208
- currentTrackIndex: ({ event }) => (event as { type: 'GOTO'; index: number }).index,
209
- }),
372
+ actions: [
373
+ 'deactivateCurrent',
374
+ 'cancelAllGapless',
375
+ 'gotoTrackIndex',
376
+ 'seekCurrentToZero',
377
+ 'preloadAhead',
378
+ ],
210
379
  },
211
380
  ],
212
381
  },