@@ -5,9 +5,14 @@ import { parseArgs } from "node:util"
5
5
import { readFileSync , existsSync } from "node:fs"
6
6
import { join } from "node:path"
7
7
8
- import { getSchedule , getSpeakers } from "@/app/conf/_api/sched-client"
8
+ import {
9
+ getSchedule ,
10
+ getSpeakerDetails ,
11
+ getSpeakers ,
12
+ RequestContext ,
13
+ } from "@/app/conf/_api/sched-client"
9
14
import { SchedSpeaker , ScheduleSession } from "@/app/conf/_api/sched-types"
10
- import { writeFile } from "node:fs/promises"
15
+ import { readFile , writeFile } from "node:fs/promises"
11
16
12
17
// Sched API rate limit is 30 requests per minute per token.
13
18
// This scripts fires:
@@ -27,6 +32,8 @@ const options = {
27
32
} ,
28
33
}
29
34
35
+ const unsafeKeys = Object . keys as < T extends object > ( obj : T ) => Array < keyof T >
36
+
30
37
async function main ( ) {
31
38
try {
32
39
const { values } = parseArgs ( { options } )
@@ -58,19 +65,6 @@ if (require.main === module) {
58
65
void main ( )
59
66
}
60
67
61
- function readJsonFile < T > ( filePath : string , defaultValue : T ) : T {
62
- if ( ! existsSync ( filePath ) ) {
63
- return defaultValue
64
- }
65
-
66
- try {
67
- const content = readFileSync ( filePath , "utf-8" )
68
- return JSON . parse ( content )
69
- } catch {
70
- return defaultValue
71
- }
72
- }
73
-
74
68
async function sync ( year : number , token : string ) {
75
69
const apiUrl = {
76
70
2023 : "https://graphqlconf23.sched.com/api" ,
@@ -80,21 +74,25 @@ async function sync(year: number, token: string) {
80
74
81
75
assert ( apiUrl , `API URL for year ${ year } not found` )
82
76
83
- const ctx = { apiUrl, token }
77
+ const ctx : RequestContext = { apiUrl, token }
84
78
85
79
const scriptDir = __dirname
86
80
const speakersFilePath = join ( scriptDir , "speakers.json" )
87
81
const scheduleFilePath = join ( scriptDir , `schedule-${ year } .json` )
88
82
89
- const existingSpeakers = readJsonFile < SchedSpeaker [ ] > ( speakersFilePath , [ ] )
90
- const existingSchedule = readJsonFile < ScheduleSession [ ] > ( scheduleFilePath , [ ] )
91
-
92
83
console . log ( "Getting schedule and speakers list..." )
93
84
94
85
const schedule = getSchedule ( ctx )
95
86
const speakers = getSpeakers ( ctx )
96
-
97
- const scheduleComparison = compare ( existingSchedule , await schedule , "id" )
87
+ const existingSchedule = readFile ( scheduleFilePath , "utf-8" ) . then ( JSON . parse )
88
+ const existingSpeakers = readFile ( speakersFilePath , "utf-8" ) . then ( JSON . parse )
89
+
90
+ const scheduleComparison = compare (
91
+ await existingSpeakers ,
92
+ await schedule ,
93
+ "id" ,
94
+ { merge : false } ,
95
+ )
98
96
printComparison ( scheduleComparison , "sessions" , "id" )
99
97
100
98
const writeSchedule = writeFile (
@@ -103,37 +101,81 @@ async function sync(year: number, token: string) {
103
101
)
104
102
105
103
const speakerComparison = compare (
106
- existingSpeakers ,
104
+ await existingSpeakers ,
107
105
await speakers ,
108
106
"username" ,
107
+ { merge : true } ,
108
+ )
109
+
110
+ await updateSpeakerDetails (
111
+ ctx ,
112
+ speakerComparison ,
113
+ SPEAKER_DETAILS_REQUEST_QUOTA ,
109
114
)
115
+
110
116
printComparison ( speakerComparison , "speakers" , "username" )
111
117
112
118
const updatedSpeakers = [
113
119
...speakerComparison . removed ,
114
120
...speakerComparison . unchanged ,
115
121
...speakerComparison . changed . map ( change => change . new ) ,
116
- ...speakerComparison . added . map ( speaker => ( {
117
- ...speaker ,
118
- [ "~syncedAt" ] : - 1 ,
119
- } ) ) ,
122
+ ...speakerComparison . added ,
120
123
] . sort ( ( a , b ) => a . username . localeCompare ( b . username ) )
121
124
122
125
const writeSpeakers = writeFile (
123
126
speakersFilePath ,
124
127
JSON . stringify ( updatedSpeakers , null , 2 ) ,
125
128
)
126
129
127
- writeSpeakers . then ( ( ) => {
128
- console . log (
129
- `Updated speakers data: ${ updatedSpeakers . length } total speakers` ,
130
- )
131
- } )
132
-
133
130
await writeSchedule
134
131
await writeSpeakers
135
132
}
136
133
134
+ async function updateSpeakerDetails (
135
+ ctx : RequestContext ,
136
+ /** mutated in place */
137
+ comparison : Comparison < SchedSpeaker > ,
138
+ quota : number ,
139
+ ) {
140
+ const locations = new Map <
141
+ string /* username */ ,
142
+ [ key : keyof Comparison < unknown > , index : number ]
143
+ > ( )
144
+
145
+ for ( const key of unsafeKeys ( comparison ) ) {
146
+ const items = comparison [ key as keyof Comparison < SchedSpeaker > ]
147
+ for ( let i = 0 ; i < items . length ; i ++ ) {
148
+ let item = items [ i ]
149
+ if ( ! ( "username" in item ) ) item = item . new
150
+ locations . set ( item . username , [ key , i ] )
151
+ }
152
+ }
153
+
154
+ const byUpdateTime = [
155
+ ...comparison . unchanged ,
156
+ ...comparison . changed . map ( change => change . new ) ,
157
+ ...comparison . added ,
158
+ ] . sort ( ( a , b ) => {
159
+ const aTime = a [ "~syncedDetailsAt" ] ?? 0
160
+ const bTime = b [ "~syncedDetailsAt" ] ?? 0
161
+ return bTime - aTime
162
+ } )
163
+
164
+ const toUpdate = byUpdateTime . slice ( 0 , quota )
165
+
166
+ const updated = await Promise . all (
167
+ toUpdate . map ( speaker => getSpeakerDetails ( ctx , speaker . username ) ) ,
168
+ )
169
+
170
+ for ( const speaker of updated ) {
171
+ const location = locations . get ( speaker . username )
172
+ if ( location ) {
173
+ const [ key , index ] = location
174
+ comparison [ key ] [ index ] = speaker
175
+ }
176
+ }
177
+ }
178
+
137
179
function help ( ) {
138
180
return console . log ( "Usage: tsx sync.ts --year <year>" )
139
181
}
@@ -148,7 +190,12 @@ type Comparison<T> = {
148
190
unchanged : T [ ]
149
191
}
150
192
151
- function compare < T extends object > ( olds : T [ ] , news : T [ ] , key : keyof T ) {
193
+ function compare < T extends object > (
194
+ olds : T [ ] ,
195
+ news : T [ ] ,
196
+ key : keyof T ,
197
+ options : { merge : boolean } ,
198
+ ) {
152
199
const oldMap = new Map ( olds . map ( o => [ o [ key ] , o ] ) )
153
200
const newMap = new Map ( news . map ( n => [ n [ key ] , n ] ) )
154
201
@@ -163,7 +210,10 @@ function compare<T extends object>(olds: T[], news: T[], key: keyof T) {
163
210
if ( JSON . stringify ( oldItem ) === JSON . stringify ( newItem ) ) {
164
211
unchanged . push ( oldItem )
165
212
} else {
166
- changed . push ( { old : oldItem , new : newItem } )
213
+ changed . push ( {
214
+ old : oldItem ,
215
+ new : options . merge ? { ...oldItem , ...newItem } : newItem ,
216
+ } )
167
217
}
168
218
} else {
169
219
added . push ( newItem )
@@ -211,10 +261,7 @@ function printComparison<T extends object>(
211
261
212
262
function objectDiff < T extends object > ( change : Change < T > ) : string {
213
263
const allKeys = [
214
- ...new Set ( [
215
- ...( Object . keys ( change . old ) as Array < keyof T > ) ,
216
- ...( Object . keys ( change . new ) as Array < keyof T > ) ,
217
- ] ) ,
264
+ ...new Set ( [ ...unsafeKeys ( change . old ) , ...unsafeKeys ( change . new ) ] ) ,
218
265
] . sort ( )
219
266
220
267
const diff = allKeys
0 commit comments