@dereekb/firebase 13.40.0 → 13.42.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 (33) hide show
  1. package/eslint/index.esm.js +145 -170
  2. package/eslint/package.json +3 -3
  3. package/index.esm.js +2865 -551
  4. package/package.json +5 -5
  5. package/src/lib/common/firestore/snapshot/snapshot.field.d.ts +4 -0
  6. package/src/lib/common/storage/context.d.ts +9 -1
  7. package/src/lib/common/storage/driver/accessor.d.ts +12 -0
  8. package/src/lib/common/storage/index.d.ts +1 -0
  9. package/src/lib/common/storage/storage.url.d.ts +69 -0
  10. package/src/lib/model/calendar/calendar.action.d.ts +34 -0
  11. package/src/lib/model/calendar/calendar.api.d.ts +147 -0
  12. package/src/lib/model/calendar/calendar.api.error.d.ts +24 -0
  13. package/src/lib/model/calendar/calendar.d.ts +460 -0
  14. package/src/lib/model/calendar/calendar.expand.d.ts +95 -0
  15. package/src/lib/model/calendar/calendar.ics.d.ts +322 -0
  16. package/src/lib/model/calendar/calendar.id.d.ts +110 -0
  17. package/src/lib/model/calendar/calendar.processing.d.ts +77 -0
  18. package/src/lib/model/calendar/calendar.query.d.ts +76 -0
  19. package/src/lib/model/calendar/calendar.schedule.d.ts +82 -0
  20. package/src/lib/model/calendar/calendar.type.d.ts +188 -0
  21. package/src/lib/model/calendar/calendar.util.d.ts +485 -0
  22. package/src/lib/model/calendar/index.d.ts +12 -0
  23. package/src/lib/model/index.d.ts +1 -0
  24. package/src/lib/model/notification/notification.message.d.ts +83 -0
  25. package/src/lib/model/notification/notification.query.d.ts +41 -0
  26. package/src/lib/model/oidcmodel/oidcmodel.query.d.ts +36 -0
  27. package/src/lib/model/storagefile/storagefile.api.d.ts +23 -2
  28. package/src/lib/model/storagefile/storagefile.create.d.ts +14 -3
  29. package/src/lib/model/storagefile/storagefile.query.d.ts +36 -0
  30. package/src/lib/model/system/index.d.ts +1 -0
  31. package/src/lib/model/system/system.scheduler.d.ts +235 -0
  32. package/test/index.esm.js +24 -1
  33. package/test/package.json +6 -6
@@ -9,6 +9,7 @@
9
9
  * before dispatching them through the configured delivery channels.
10
10
  */
11
11
  import { type PromiseOrValue, type Maybe, type WebsiteUrl, type NameEmailPair, type ArrayOrValue } from '@dereekb/util';
12
+ import { type ICalendarIcsString, type ICalendarMethod } from '@dereekb/date';
12
13
  import { type NotificationRecipient, type NotificationRecipientWithConfig } from './notification.config';
13
14
  import { type NotificationSendFlags, type Notification, type NotificationBox } from './notification';
14
15
  import { type NotificationItem, type NotificationItemMetadata } from './notification.item';
@@ -102,6 +103,80 @@ export interface NotificationMessageContent {
102
103
  */
103
104
  readonly templateVariables?: Maybe<NotificationMessageTemplateVariables>;
104
105
  }
106
+ /**
107
+ * The iTIP method a {@link NotificationMessageCalendarAttachment} may carry.
108
+ *
109
+ * Deliberately the whole of {@link ICalendarMethod}, including its open string branch. A notification
110
+ * USUALLY speaks as the organizer -- PUBLISH for an informational copy, REQUEST for an invitation or an
111
+ * update to one, ADD for extra instances of a recurring event, CANCEL to withdraw one, and DECLINECOUNTER
112
+ * to reject a proposed change -- but an app that sends ON BEHALF of an attendee has an equally real use
113
+ * for the attendee-to-organizer methods: REPLY to RSVP, REFRESH to ask for the latest copy, and COUNTER
114
+ * to propose one. Narrowing to the organizer set would put that behind a library change, and closing the
115
+ * union would also drop the `X-` extension methods {@link ICalendarMethod} intentionally leaves room for.
116
+ *
117
+ * The invariant worth enforcing is not WHICH method but that it agrees with the METHOD property inside
118
+ * the payload, which no type can express -- so this alias exists to document the choice rather than to
119
+ * constrain it.
120
+ */
121
+ export type NotificationMessageCalendarAttachmentMethod = ICalendarMethod;
122
+ /**
123
+ * A rendered iTIP calendar payload for a single recipient, for the sending service to bundle as a calendar
124
+ * MIME part on the outgoing email.
125
+ *
126
+ * Produced by a {@link NotificationMessageCalendarAttachmentFactory} at SEND time and never stored: it is
127
+ * not on `NotificationItem.d`, because the item is re-embedded verbatim into `NotificationSummary.n[]`
128
+ * (capped at 1000 items) and `NotificationWeek.n[]`, which each share a single 1 MiB document — an ICS
129
+ * blob in `d` would consume the summary's whole budget. Store the IDENTIFIERS in `d` and render the ICS
130
+ * from the message factory, which is async and holds the notification document.
131
+ */
132
+ export interface NotificationMessageCalendarAttachment {
133
+ /**
134
+ * The rendered ICS document.
135
+ */
136
+ readonly ics: ICalendarIcsString;
137
+ /**
138
+ * The iTIP method the document carries. Duplicated onto the part's Content-Type by the sending service,
139
+ * as RFC 6047 requires, and it MUST agree with the METHOD property inside {@link ics}.
140
+ */
141
+ readonly method: NotificationMessageCalendarAttachmentMethod;
142
+ /**
143
+ * File name of the part. Defaults to "invite.ics" when the sending service is given none.
144
+ */
145
+ readonly filename?: Maybe<string>;
146
+ }
147
+ /**
148
+ * The file name given to a {@link NotificationMessageCalendarAttachment} that carries none.
149
+ */
150
+ export declare const DEFAULT_NOTIFICATION_MESSAGE_CALENDAR_ATTACHMENT_FILENAME = "invite.ics";
151
+ /**
152
+ * Input for a {@link NotificationMessageCalendarAttachmentFactory}.
153
+ */
154
+ export interface NotificationMessageCalendarAttachmentFactoryInput {
155
+ /**
156
+ * The message the part is being built for.
157
+ */
158
+ readonly message: NotificationMessage;
159
+ /**
160
+ * The address the sending service has resolved for this recipient, and the address the payload's
161
+ * ATTENDEE must name.
162
+ *
163
+ * Load-bearing for a REQUEST: a client only renders an invitation inline when it finds ITS OWN address
164
+ * in the ATTENDEE, which is why the payload is built here rather than once per notification.
165
+ */
166
+ readonly recipient: NameEmailPair;
167
+ }
168
+ /**
169
+ * Renders the iTIP calendar payload for one recipient of a message.
170
+ *
171
+ * A factory rather than a value because the payload is per-recipient and transient: it is never
172
+ * serialized, it is only meaningful to a sending service that delivers email, and the ATTENDEE varies by
173
+ * recipient. Deferring the render to the sending service means one of these can be built once per
174
+ * notification, closing over the event, and the ICS is only produced for the recipients that actually
175
+ * receive an email.
176
+ *
177
+ * Return `undefined` to send the email without a calendar part.
178
+ */
179
+ export type NotificationMessageCalendarAttachmentFactory = (input: NotificationMessageCalendarAttachmentFactoryInput) => PromiseOrValue<Maybe<NotificationMessageCalendarAttachment>>;
105
180
  export interface NotificationMessageEmailContent extends NotificationMessageContent {
106
181
  /**
107
182
  * Email subject. If not defined, defaults to the title.
@@ -133,6 +208,14 @@ export interface NotificationMessageEmailContent extends NotificationMessageCont
133
208
  * If the "replyTo" is present, this value acts as a fallback if the entity key returns no match.
134
209
  */
135
210
  readonly replyToEmail?: Maybe<NameEmailPair>;
211
+ /**
212
+ * Renders an iTIP calendar payload to bundle onto the email as a calendar MIME part.
213
+ *
214
+ * Opt-in per sending service, like every other field here: a builder that does not call it simply sends
215
+ * the email without the invite. A builder that DOES call it must give each recipient the payload names a
216
+ * request of its own, since attachments live on the request rather than the recipient.
217
+ */
218
+ readonly calendarAttachmentFactory?: Maybe<NotificationMessageCalendarAttachmentFactory>;
136
219
  }
137
220
  export interface NotificationMessageNotificationSummaryContent {
138
221
  }
@@ -13,6 +13,11 @@ import { type ArrayOrValue } from '@dereekb/util';
13
13
  * Used by the server to discover users whose configs need to be synced to their NotificationBox recipients.
14
14
  *
15
15
  * @returns Array of Firestore query constraints filtering for users needing sync.
16
+ *
17
+ * @dbxModelFirebaseIndex
18
+ * @dbxModelFirebaseIndexModel NotificationUser
19
+ * @dbxModelFirebaseIndexScope COLLECTION
20
+ * @dbxModelFirebaseIndexCategory sweep
16
21
  */
17
22
  export declare function notificationUsersFlaggedForNeedsSyncQuery(): FirestoreQueryConstraint[];
18
23
  /**
@@ -20,18 +25,34 @@ export declare function notificationUsersFlaggedForNeedsSyncQuery(): FirestoreQu
20
25
  *
21
26
  * @param exclusionId - One or more box IDs or collection name prefixes to match against.
22
27
  * @returns Array of Firestore query constraints filtering for users with matching exclusions.
28
+ *
29
+ * @dbxModelFirebaseIndex
30
+ * @dbxModelFirebaseIndexModel NotificationUser
31
+ * @dbxModelFirebaseIndexScope COLLECTION
32
+ * @dbxModelFirebaseIndexCategory lookup
33
+ * @dbxModelFirebaseIndexAllowArrayContainsAny
23
34
  */
24
35
  export declare function notificationUserHasExclusionQuery(exclusionId: ArrayOrValue<NotificationBoxSendExclusion>): FirestoreQueryConstraint[];
25
36
  /**
26
37
  * Query constraints for finding {@link NotificationSummary} documents that need server-side initialization (`s == true`).
27
38
  *
28
39
  * @returns Array of Firestore query constraints filtering for summaries needing initialization.
40
+ *
41
+ * @dbxModelFirebaseIndex
42
+ * @dbxModelFirebaseIndexModel NotificationSummary
43
+ * @dbxModelFirebaseIndexScope COLLECTION
44
+ * @dbxModelFirebaseIndexCategory init
29
45
  */
30
46
  export declare function notificationSummariesFlaggedForNeedsInitializationQuery(): FirestoreQueryConstraint[];
31
47
  /**
32
48
  * Query constraints for finding {@link NotificationBox} documents that need server-side initialization (`s == true`).
33
49
  *
34
50
  * @returns Array of Firestore query constraints filtering for boxes needing initialization.
51
+ *
52
+ * @dbxModelFirebaseIndex
53
+ * @dbxModelFirebaseIndexModel NotificationBox
54
+ * @dbxModelFirebaseIndexScope COLLECTION
55
+ * @dbxModelFirebaseIndexCategory init
35
56
  */
36
57
  export declare function notificationBoxesFlaggedForNeedsInitializationQuery(): FirestoreQueryConstraint[];
37
58
  /**
@@ -40,6 +61,11 @@ export declare function notificationBoxesFlaggedForNeedsInitializationQuery(): F
40
61
  * Used by the server to clean up boxes that could not be initialized.
41
62
  *
42
63
  * @returns Array of Firestore query constraints filtering for boxes flagged as invalid.
64
+ *
65
+ * @dbxModelFirebaseIndex
66
+ * @dbxModelFirebaseIndexModel NotificationBox
67
+ * @dbxModelFirebaseIndexScope COLLECTION
68
+ * @dbxModelFirebaseIndexCategory cleanup
43
69
  */
44
70
  export declare function notificationBoxesFlaggedInvalidQuery(): FirestoreQueryConstraint[];
45
71
  /**
@@ -50,6 +76,11 @@ export declare function notificationBoxesFlaggedInvalidQuery(): FirestoreQueryCo
50
76
  *
51
77
  * @param now - Reference time for the `sat` comparison (defaults to current time)
52
78
  * @returns Array of Firestore query constraints filtering for notifications past their scheduled send time.
79
+ *
80
+ * @dbxModelFirebaseIndex
81
+ * @dbxModelFirebaseIndexModel Notification
82
+ * @dbxModelFirebaseIndexScope COLLECTION_GROUP
83
+ * @dbxModelFirebaseIndexCategory sweep
53
84
  */
54
85
  export declare function notificationsPastSendAtTimeQuery(now?: Date): FirestoreQueryConstraint[];
55
86
  /**
@@ -57,6 +88,11 @@ export declare function notificationsPastSendAtTimeQuery(now?: Date): FirestoreQ
57
88
  * and ready to be archived to {@link NotificationWeek} and then deleted.
58
89
  *
59
90
  * @returns Array of Firestore query constraints filtering for completed notifications ready to archive.
91
+ *
92
+ * @dbxModelFirebaseIndex
93
+ * @dbxModelFirebaseIndexModel Notification
94
+ * @dbxModelFirebaseIndexScope COLLECTION_GROUP
95
+ * @dbxModelFirebaseIndexCategory cleanup
60
96
  */
61
97
  export declare function notificationsReadyForCleanupQuery(): FirestoreQueryConstraint[];
62
98
  /**
@@ -74,5 +110,10 @@ export declare function notificationsReadyForCleanupQuery(): FirestoreQueryConst
74
110
  * @param retentionDays - Number of days of history to retain; days strictly older than `now - retentionDays` match.
75
111
  * @param now - Reference time for the cutoff (defaults to current time)
76
112
  * @returns Array of Firestore query constraints filtering by the day string.
113
+ *
114
+ * @dbxModelFirebaseIndex
115
+ * @dbxModelFirebaseIndexModel NotificationLoggedEventDay
116
+ * @dbxModelFirebaseIndexScope COLLECTION_GROUP
117
+ * @dbxModelFirebaseIndexCategory cleanup
77
118
  */
78
119
  export declare function notificationLoggedEventDaysOlderThanQuery(retentionDays: number, now?: Date): FirestoreQueryConstraint[];
@@ -5,6 +5,11 @@ import { type OidcEntryType } from './oidcmodel';
5
5
  *
6
6
  * @param type - The OIDC entry type to filter by.
7
7
  * @returns Firestore query constraints for the given type.
8
+ *
9
+ * @dbxModelFirebaseIndex
10
+ * @dbxModelFirebaseIndexModel OidcEntry
11
+ * @dbxModelFirebaseIndexScope COLLECTION
12
+ * @dbxModelFirebaseIndexCategory lookup
8
13
  */
9
14
  export declare function oidcEntriesWithTypeQuery(type: OidcEntryType): FirestoreQueryConstraint[];
10
15
  /**
@@ -13,6 +18,11 @@ export declare function oidcEntriesWithTypeQuery(type: OidcEntryType): Firestore
13
18
  * @param type - The OIDC entry type to filter by.
14
19
  * @param userCode - The user code to match.
15
20
  * @returns Firestore query constraints for the given type and userCode.
21
+ *
22
+ * @dbxModelFirebaseIndex
23
+ * @dbxModelFirebaseIndexModel OidcEntry
24
+ * @dbxModelFirebaseIndexScope COLLECTION
25
+ * @dbxModelFirebaseIndexCategory lookup
16
26
  */
17
27
  export declare function oidcEntriesByUserCodeQuery(type: OidcEntryType, userCode: string): FirestoreQueryConstraint[];
18
28
  /**
@@ -21,6 +31,11 @@ export declare function oidcEntriesByUserCodeQuery(type: OidcEntryType, userCode
21
31
  * @param type - The OIDC entry type to filter by.
22
32
  * @param uid - The Firebase user ID to match.
23
33
  * @returns Firestore query constraints for the given type and uid.
34
+ *
35
+ * @dbxModelFirebaseIndex
36
+ * @dbxModelFirebaseIndexModel OidcEntry
37
+ * @dbxModelFirebaseIndexScope COLLECTION
38
+ * @dbxModelFirebaseIndexCategory lookup
24
39
  */
25
40
  export declare function oidcEntriesByUidQuery(type: OidcEntryType, uid: FirebaseAuthUserId): FirestoreQueryConstraint[];
26
41
  /**
@@ -29,6 +44,11 @@ export declare function oidcEntriesByUidQuery(type: OidcEntryType, uid: Firebase
29
44
  * @param type - The OIDC entry type to filter by.
30
45
  * @param grantId - The grant ID to match.
31
46
  * @returns Firestore query constraints for the given type and grantId.
47
+ *
48
+ * @dbxModelFirebaseIndex
49
+ * @dbxModelFirebaseIndexModel OidcEntry
50
+ * @dbxModelFirebaseIndexScope COLLECTION
51
+ * @dbxModelFirebaseIndexCategory lookup
32
52
  */
33
53
  export declare function oidcEntriesByGrantIdQuery(type: OidcEntryType, grantId: string): FirestoreQueryConstraint[];
34
54
  /**
@@ -37,6 +57,11 @@ export declare function oidcEntriesByGrantIdQuery(type: OidcEntryType, grantId:
37
57
  * @param type - The OIDC entry type to filter by.
38
58
  * @param clientId - The OAuth client ID to match.
39
59
  * @returns Firestore query constraints for the given type and clientId.
60
+ *
61
+ * @dbxModelFirebaseIndex
62
+ * @dbxModelFirebaseIndexModel OidcEntry
63
+ * @dbxModelFirebaseIndexScope COLLECTION
64
+ * @dbxModelFirebaseIndexCategory lookup
40
65
  */
41
66
  export declare function oidcEntriesByClientIdQuery(type: OidcEntryType, clientId: string): FirestoreQueryConstraint[];
42
67
  /**
@@ -44,6 +69,11 @@ export declare function oidcEntriesByClientIdQuery(type: OidcEntryType, clientId
44
69
  *
45
70
  * @param ownershipKey - The ownership key identifying the owner.
46
71
  * @returns Firestore query constraints for Client entries matching the ownership key.
72
+ *
73
+ * @dbxModelFirebaseIndex
74
+ * @dbxModelFirebaseIndexModel OidcEntry
75
+ * @dbxModelFirebaseIndexScope COLLECTION
76
+ * @dbxModelFirebaseIndexCategory lookup
47
77
  */
48
78
  export declare function oidcClientEntriesByOwnerQuery(ownershipKey: FirebaseAuthOwnershipKey): FirestoreQueryConstraint[];
49
79
  /**
@@ -54,5 +84,11 @@ export declare function oidcClientEntriesByOwnerQuery(ownershipKey: FirebaseAuth
54
84
  *
55
85
  * @param uid - The Firebase user id the grants were issued to.
56
86
  * @returns Firestore query constraints for Grant entries matching the uid.
87
+ *
88
+ * @dbxModelFirebaseIndex
89
+ * @dbxModelFirebaseIndexModel OidcEntry
90
+ * @dbxModelFirebaseIndexScope COLLECTION
91
+ * @dbxModelFirebaseIndexCategory lookup
92
+ * @dbxModelFirebaseIndexDispatcher true
57
93
  */
58
94
  export declare function oidcGrantEntriesByUidQuery(uid: FirebaseAuthUserId): FirestoreQueryConstraint[];
@@ -51,13 +51,34 @@ export declare const initializeStorageFileFromUploadParamsType: Type<InitializeS
51
51
  /**
52
52
  * Parameters for triggering processing of a specific StorageFile.
53
53
  *
54
- * Supports various modes: immediate processing, retry checking, force restart,
55
- * and reprocessing already-successful files. Validated with {@link processStorageFileParamsType}.
54
+ * Which flag is required depends on the file's current processing state. A `FAILED` file
55
+ * restarts with no flag, while a `SUCCESS` file needs `processAgainIfSuccessful` (or
56
+ * `forceRestartProcessing`) — note that a file whose processor ran to completion is `SUCCESS`
57
+ * even when the outcome was a rejection, so re-validating a rejected file normally needs one of
58
+ * those flags. `ARCHIVED` and `DO_NOT_PROCESS` files cannot be processed at all.
59
+ *
60
+ * Validated with {@link processStorageFileParamsType}.
56
61
  */
57
62
  export interface ProcessStorageFileParams extends TargetModelParams {
63
+ /**
64
+ * Runs the first step of the processing task inline instead of waiting for the scheduled task runner.
65
+ */
58
66
  readonly runImmediately?: Maybe<boolean>;
67
+ /**
68
+ * Checks an in-flight `PROCESSING` task immediately, instead of waiting for it to age past the
69
+ * stuck-check throttle.
70
+ */
59
71
  readonly checkRetryProcessing?: Maybe<boolean>;
72
+ /**
73
+ * Abandons the file's existing processing task and begins a new one, clearing the completed
74
+ * checkpoints so the flow runs again from the start. For a `PROCESSING` file this is only applied
75
+ * once the retry check runs, so pair it with `checkRetryProcessing` to force a restart while the
76
+ * existing task is still within the stuck-check throttle.
77
+ */
60
78
  readonly forceRestartProcessing?: Maybe<boolean>;
79
+ /**
80
+ * Allows processing a file that has already finished processing and is in the `SUCCESS` state.
81
+ */
61
82
  readonly processAgainIfSuccessful?: Maybe<boolean>;
62
83
  }
63
84
  export declare const processStorageFileParamsType: Type<ProcessStorageFileParams>;
@@ -5,7 +5,7 @@ import { type FirestoreDocumentAccessor } from '../../common/firestore/accessor/
5
5
  import { type FirebaseStorageAccessorFile } from '../../common/storage/driver/accessor';
6
6
  import { type StoragePathRef, type StoragePath } from '../../common/storage/storage';
7
7
  import { type FirebaseAuthOwnershipKey, type FirebaseAuthUserId } from '../../common/auth/auth';
8
- import { type StorageFilePurposeSubgroup, type StorageFileGroupId, type StorageFileGroupRelatedStorageFilePurpose, type StorageFileMetadata, type StorageFilePurpose } from './storagefile.id';
8
+ import { type StorageFilePurposeSubgroup, type StorageFileGroupId, type StorageFileGroupRelatedStorageFilePurpose, type StorageFileId, type StorageFileMetadata, type StorageFilePurpose } from './storagefile.id';
9
9
  import { type ReadFirestoreModelKeyInput } from '../../common';
10
10
  /**
11
11
  * Input for creating a StorageFile document paired with its storage path.
@@ -13,8 +13,8 @@ import { type ReadFirestoreModelKeyInput } from '../../common';
13
13
  * Provides all the fields needed to create a fully-configured {@link StorageFile} Firestore document,
14
14
  * including ownership, purpose, group membership, and processing state.
15
15
  *
16
- * Either a `file`, `storagePathRef`, or `storagePath` must be provided to identify the storage location.
17
- * Either an `accessor` or `context` must be provided for Firestore document creation.
16
+ * Either a `file`, `storagePathRef`, `storagePath`, or `storagePathFactory` must be provided to identify the
17
+ * storage location. Either an `accessor` or `context` must be provided for Firestore document creation.
18
18
  *
19
19
  * @template M - type of arbitrary metadata stored in the `d` field
20
20
  */
@@ -43,6 +43,17 @@ export interface CreateStorageFileDocumentPairInput<M extends StorageFileMetadat
43
43
  * File to use when creating the StorageFile.
44
44
  */
45
45
  readonly file?: FirebaseStorageAccessorFile;
46
+ /**
47
+ * Builds the storage path from the id of the StorageFile document being created.
48
+ *
49
+ * Use this when the path should be keyed by the StorageFile itself rather than by the model it belongs
50
+ * to. Two StorageFiles then can never resolve to the same path, so replacing one cannot delete the
51
+ * other's content, and the path is not guessable from the owning model's id.
52
+ *
53
+ * Only consulted when no `file`, `storagePathRef`, or `storagePath` is provided, and only for a
54
+ * DIRECTLY_CREATED StorageFile — a FOR_STORAGE_FILE_GROUP file has a deterministic id by design.
55
+ */
56
+ readonly storagePathFactory?: Maybe<(storageFileId: StorageFileId) => StoragePath>;
46
57
  /**
47
58
  * The display name of the StorageFile.
48
59
  *
@@ -9,6 +9,11 @@ import { type FirebaseAuthUserId } from '../../common/auth/auth';
9
9
  *
10
10
  * @returns Firestore query constraints for StorageFiles queued for processing.
11
11
  *
12
+ * @dbxModelFirebaseIndex
13
+ * @dbxModelFirebaseIndexModel StorageFile
14
+ * @dbxModelFirebaseIndexScope COLLECTION
15
+ * @dbxModelFirebaseIndexCategory sweep
16
+ *
12
17
  * @example
13
18
  * ```ts
14
19
  * const constraints = storageFilesQueuedForProcessingQuery();
@@ -24,6 +29,11 @@ export declare function storageFilesQueuedForProcessingQuery(): FirestoreQueryCo
24
29
  * @param now - Reference time for comparison; defaults to current time.
25
30
  * @returns Firestore query constraints for StorageFiles whose scheduled delete date has passed.
26
31
  *
32
+ * @dbxModelFirebaseIndex
33
+ * @dbxModelFirebaseIndexModel StorageFile
34
+ * @dbxModelFirebaseIndexScope COLLECTION
35
+ * @dbxModelFirebaseIndexCategory cleanup
36
+ *
27
37
  * @example
28
38
  * ```ts
29
39
  * const constraints = storageFilesQueuedForDeleteQuery();
@@ -48,6 +58,12 @@ export interface StorageFilePurposeAndUserQueryInput {
48
58
  * @param input - The user, purpose, and optional subgroup to filter by.
49
59
  * @returns Firestore query constraints for the given purpose and user.
50
60
  *
61
+ * @dbxModelFirebaseIndex
62
+ * @dbxModelFirebaseIndexModel StorageFile
63
+ * @dbxModelFirebaseIndexScope COLLECTION
64
+ * @dbxModelFirebaseIndexCategory lookup
65
+ * @dbxModelFirebaseIndexSkip true
66
+ *
51
67
  * @example
52
68
  * @example
53
69
  * ```ts
@@ -65,6 +81,11 @@ export declare function storageFilePurposeAndUserQuery(input: StorageFilePurpose
65
81
  *
66
82
  * @returns Firestore query constraints for StorageFiles flagged for group synchronization.
67
83
  *
84
+ * @dbxModelFirebaseIndex
85
+ * @dbxModelFirebaseIndexModel StorageFile
86
+ * @dbxModelFirebaseIndexScope COLLECTION
87
+ * @dbxModelFirebaseIndexCategory sweep
88
+ *
68
89
  * @example
69
90
  * @example
70
91
  * ```ts
@@ -79,6 +100,11 @@ export declare function storageFileFlaggedForSyncWithGroupsQuery(): FirestoreQue
79
100
  *
80
101
  * @returns Firestore query constraints for StorageFileGroups needing initialization.
81
102
  *
103
+ * @dbxModelFirebaseIndex
104
+ * @dbxModelFirebaseIndexModel StorageFileGroup
105
+ * @dbxModelFirebaseIndexScope COLLECTION
106
+ * @dbxModelFirebaseIndexCategory init
107
+ *
82
108
  * @example
83
109
  * @example
84
110
  * ```ts
@@ -91,6 +117,11 @@ export declare function storageFileGroupsFlaggedForNeedsInitializationQuery(): F
91
117
  *
92
118
  * @returns Firestore query constraints for StorageFileGroups flagged for content regeneration.
93
119
  *
120
+ * @dbxModelFirebaseIndex
121
+ * @dbxModelFirebaseIndexModel StorageFileGroup
122
+ * @dbxModelFirebaseIndexScope COLLECTION
123
+ * @dbxModelFirebaseIndexCategory sweep
124
+ *
94
125
  * @example
95
126
  * @example
96
127
  * ```ts
@@ -105,6 +136,11 @@ export declare function storageFileGroupsFlaggedForContentRegenerationQuery(): F
105
136
  *
106
137
  * @returns Firestore query constraints for StorageFileGroups flagged as invalid.
107
138
  *
139
+ * @dbxModelFirebaseIndex
140
+ * @dbxModelFirebaseIndexModel StorageFileGroup
141
+ * @dbxModelFirebaseIndexScope COLLECTION
142
+ * @dbxModelFirebaseIndexCategory cleanup
143
+ *
108
144
  * @example
109
145
  * @example
110
146
  * ```ts
@@ -1,2 +1,3 @@
1
1
  export * from './system';
2
2
  export * from './system.action';
3
+ export * from './system.scheduler';
@@ -0,0 +1,235 @@
1
+ /**
2
+ * @module system.scheduler
3
+ *
4
+ * Declares the framework-owned `scheduler` {@link SystemState} type, plus the pure predicates and
5
+ * the moment-bound read object that make up the "has this schedule already run in this hour?" gate.
6
+ *
7
+ * The gate exists so a cron that fires every hour can run a body that should only run every Nth
8
+ * hour, without each individual task having to carry a throttle of its own. Evaluate it ONCE at the
9
+ * top of a schedule function: pass, run the work; fail, return.
10
+ *
11
+ * The stateful half — read, evaluate, and claim the hour against Firestore — lives in
12
+ * `@dereekb/firebase-server/model` as `schedulerSystemStateAccessorFactory()`. Only the shape, the
13
+ * predicates, and {@link schedulerSystemStateRead} live here, so the evaluation can be exercised
14
+ * without an emulator and the converter can be registered from an app's client-shared converter
15
+ * map.
16
+ */
17
+ import { type HourOfDay, type Hours, type Maybe } from '@dereekb/util';
18
+ import { type FirestoreDocument, type FirestoreDocumentAccessor } from '../../common';
19
+ import { type SystemState, type SystemStateDocument, type SystemStateStoredData, type SystemStateStoredDataFieldConverterConfig } from './system';
20
+ /**
21
+ * {@link SystemState} type identifier for the scheduler's run-gate state.
22
+ *
23
+ * Also the document id, per the SystemState singleton convention — so this state lives at
24
+ * `sys/scheduler`.
25
+ */
26
+ export declare const SCHEDULER_SYSTEM_STATE_TYPE = "scheduler";
27
+ /**
28
+ * Scheduler state for gating scheduled work to at most one run per hour window.
29
+ *
30
+ * ONE gate, ONE `lat`. This is deliberately not per-task and not per-table state: it answers
31
+ * "has the scheduler already run its Nth-hour body during this hour?" for the app as a whole. Two
32
+ * callers sharing the document with different `everyNHours` values will have whichever one passes
33
+ * first claim the hour for both.
34
+ *
35
+ * @dbxModelSubObject
36
+ */
37
+ export interface SchedulerSystemData extends SystemStateStoredData {
38
+ /**
39
+ * Last run at. The single gate anchor — the moment the most recent passing check claimed its hour.
40
+ *
41
+ * @dbxModelVariable lastRunAt
42
+ */
43
+ lat?: Maybe<Date>;
44
+ }
45
+ /**
46
+ * Firestore field converter for {@link SchedulerSystemData}.
47
+ *
48
+ * Register it in the app's `SystemStateStoredDataConverterMap` under
49
+ * {@link SCHEDULER_SYSTEM_STATE_TYPE}. This is NOT optional: without it the collection falls back to
50
+ * the pass-through converter, `lat` reads back as a raw Firestore `Timestamp` instead of a `Date`,
51
+ * and {@link hasRunInCurrentHour} can never match — so the gate would open on every single call.
52
+ */
53
+ export declare const schedulerSystemDataConverter: SystemStateStoredDataFieldConverterConfig<SchedulerSystemData>;
54
+ /**
55
+ * Loads the {@link SystemStateDocument} that stores {@link SchedulerSystemData}, using
56
+ * {@link SCHEDULER_SYSTEM_STATE_TYPE} as the document id.
57
+ *
58
+ * @param accessor - The document accessor for the SystemState collection.
59
+ * @returns The SystemState document for the scheduler state.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const doc = loadSchedulerSystemState(systemStateCollection.documentAccessor());
64
+ * const data = await doc.snapshotData();
65
+ * ```
66
+ */
67
+ export declare function loadSchedulerSystemState<D extends FirestoreDocument<SystemState<SystemStateStoredData>>>(accessor: FirestoreDocumentAccessor<SystemState<SystemStateStoredData>, D>): SystemStateDocument<SchedulerSystemData>;
68
+ /**
69
+ * Whether the date's hour-of-day is an Nth hour, in the ambient timezone.
70
+ *
71
+ * This is a modulo against the hour-of-day, NOT an interval since some epoch — so at hour 12 it is
72
+ * true for N = 1, 2, 3, 4, 6, and 12, and false for N = 5. Only divisors of 24 divide the day
73
+ * evenly; a non-divisor (5, 7, 9, …) will still fire, just with a short window across midnight.
74
+ *
75
+ * An `everyNHours` of zero or less has no meaningful Nth hour and returns false rather than
76
+ * dividing by zero.
77
+ *
78
+ * @param everyNHours - Run every Nth hour of the day.
79
+ * @param date - Moment to test; defaults to now.
80
+ * @returns True when the date's hour-of-day is an Nth hour.
81
+ *
82
+ * @dbxUtil
83
+ * @dbxUtilCategory date
84
+ * @dbxUtilTags schedule, hour, throttle, cron, gate, modulo
85
+ * @dbxUtilRelated has-run-in-current-hour
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * isNthHourOfDay(3, new Date('2024-01-01T12:30:00')); // true (12 % 3 === 0)
90
+ * isNthHourOfDay(5, new Date('2024-01-01T12:30:00')); // false (12 % 5 === 2)
91
+ * ```
92
+ */
93
+ export declare function isNthHourOfDay(everyNHours: Hours, date?: Maybe<Date>): boolean;
94
+ /**
95
+ * Whether the last run falls inside the same hour as now.
96
+ *
97
+ * This is a same-hour-bucket compare, not an elapsed-time compare: a run at 12:59 and a `now` of
98
+ * 13:01 are two minutes apart but in different hours, so this returns false. That is what makes the
99
+ * gate track the hourly cron rather than drift with it.
100
+ *
101
+ * A null `lastRunAt` means nothing has run, so it returns false.
102
+ *
103
+ * @param lastRunAt - Moment the work last ran.
104
+ * @param now - Moment to compare against; defaults to now.
105
+ * @returns True when both moments fall in the same hour.
106
+ *
107
+ * @dbxUtil
108
+ * @dbxUtilCategory date
109
+ * @dbxUtilTags schedule, hour, throttle, cron, gate, window
110
+ * @dbxUtilRelated is-nth-hour-of-day, round-down-to-hour
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * hasRunInCurrentHour(new Date('2024-01-01T12:05:00'), new Date('2024-01-01T12:55:00')); // true
115
+ * hasRunInCurrentHour(new Date('2024-01-01T12:59:00'), new Date('2024-01-01T13:01:00')); // false
116
+ * ```
117
+ */
118
+ export declare function hasRunInCurrentHour(lastRunAt: Maybe<Date>, now?: Maybe<Date>): boolean;
119
+ /**
120
+ * The zero-based index of the date's hour within the day's every-N-hours schedule, or null when the
121
+ * hour is not an Nth hour.
122
+ *
123
+ * At hour 12 with N=3 the day's matching hours are 0, 3, 6, 9, 12 — so the index is 4. Use it to
124
+ * rotate work across a day's windows ("do the expensive pass only on index 0") without each caller
125
+ * re-deriving the arithmetic from the hour-of-day.
126
+ *
127
+ * Returns null rather than a number for a non-matching hour, so a falsy check cannot confuse
128
+ * "index 0" — the first window of the day — with "no window".
129
+ *
130
+ * @param everyNHours - Run every Nth hour of the day.
131
+ * @param date - Moment to test; defaults to now.
132
+ * @returns The zero-based window index, or null when the hour is not an Nth hour.
133
+ *
134
+ * @dbxUtil
135
+ * @dbxUtilCategory date
136
+ * @dbxUtilTags schedule, hour, throttle, cron, gate, index, window
137
+ * @dbxUtilRelated is-nth-hour-of-day
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * nthHourOfDayIndex(3, new Date('2024-01-01T12:30:00')); // 4 (0, 3, 6, 9, 12)
142
+ * nthHourOfDayIndex(3, new Date('2024-01-01T13:30:00')); // null (13 % 3 !== 0)
143
+ * ```
144
+ */
145
+ export declare function nthHourOfDayIndex(everyNHours: Hours, date?: Maybe<Date>): Maybe<number>;
146
+ /**
147
+ * A read of the scheduler's gate state, evaluated against a single moment.
148
+ *
149
+ * Holds the `now` and the `lastRunAt` it was built from, so every question below is answered against
150
+ * the same moment. Asking about N=2 and then N=3 cannot straddle an hour boundary mid-evaluation,
151
+ * and no question re-reads the document.
152
+ *
153
+ * This is the pure half of the gate. The Firestore-backed half — read, evaluate, and CLAIM the hour
154
+ * in a transaction — is `schedulerSystemStateAccessorFactory()` in `@dereekb/firebase-server/model`.
155
+ */
156
+ export interface SchedulerSystemStateRead {
157
+ /**
158
+ * The moment this read was taken at.
159
+ */
160
+ readonly now: Date;
161
+ /**
162
+ * The hour-of-day of {@link SchedulerSystemStateRead.now}, in the ambient timezone.
163
+ */
164
+ readonly hourOfDay: HourOfDay;
165
+ /**
166
+ * The moment the most recent passing check claimed its hour, or null if the gate has never been
167
+ * claimed.
168
+ */
169
+ readonly lastRunAt: Maybe<Date>;
170
+ /**
171
+ * Whether {@link SchedulerSystemStateRead.lastRunAt} falls inside the same hour as `now` — that is,
172
+ * whether the gate has already been claimed during this hour.
173
+ */
174
+ readonly hasRunInCurrentHour: boolean;
175
+ /**
176
+ * Whether the gate is open for the given interval: `now` is an Nth hour of the day AND the gate
177
+ * has not already been claimed during this hour.
178
+ *
179
+ * This is a pure evaluation — it does NOT claim the hour. Use
180
+ * `SchedulerSystemStateAccessor.checkAndClaim()` to actually gate work.
181
+ *
182
+ * @param everyNHours - Run every Nth hour of the day.
183
+ * @returns True when the gate is open.
184
+ */
185
+ isOpen(everyNHours: Hours): boolean;
186
+ /**
187
+ * {@link isNthHourOfDay} bound to this read's `now`.
188
+ *
189
+ * Unlike {@link SchedulerSystemStateRead.isOpen} this ignores `lastRunAt` entirely, which is what
190
+ * makes it the right sub-gate for work running INSIDE an already-claimed hour: the hour is spent
191
+ * either way, so each task only needs to ask whether this is its hour.
192
+ *
193
+ * @param everyNHours - Run every Nth hour of the day.
194
+ * @returns True when this read's hour-of-day is an Nth hour.
195
+ */
196
+ isNthHourOfDay(everyNHours: Hours): boolean;
197
+ /**
198
+ * {@link nthHourOfDayIndex} bound to this read's `now`.
199
+ *
200
+ * @param everyNHours - Run every Nth hour of the day.
201
+ * @returns The zero-based window index, or null when this read's hour is not an Nth hour.
202
+ */
203
+ nthHourOfDayIndex(everyNHours: Hours): Maybe<number>;
204
+ }
205
+ /**
206
+ * Configuration for {@link schedulerSystemStateRead}.
207
+ */
208
+ export interface SchedulerSystemStateReadConfig {
209
+ /**
210
+ * The moment to evaluate the gate against. Defaults to the current time.
211
+ */
212
+ readonly now?: Maybe<Date>;
213
+ /**
214
+ * The `lat` read off the scheduler document, or null/undefined when it has never been claimed.
215
+ */
216
+ readonly lastRunAt: Maybe<Date>;
217
+ }
218
+ /**
219
+ * Creates a {@link SchedulerSystemStateRead} from a moment and a last-run.
220
+ *
221
+ * Every predicate on the result closes over the single `now` resolved here, so a caller can ask
222
+ * about any number of intervals off one read without drifting across an hour boundary.
223
+ *
224
+ * @param config - The moment and the document's last-run.
225
+ * @returns The evaluated read.
226
+ *
227
+ * @example
228
+ * ```ts
229
+ * const read = schedulerSystemStateRead({ now, lastRunAt });
230
+ *
231
+ * read.isOpen(3); // should the every-3-hours body run?
232
+ * read.isNthHourOfDay(6); // is this also a 6th hour, for a sub-gated task?
233
+ * ```
234
+ */
235
+ export declare function schedulerSystemStateRead(config: SchedulerSystemStateReadConfig): SchedulerSystemStateRead;