2
2
3
3
import assert from "node:assert"
4
4
import { parseArgs } from "node:util"
5
+ import { readFileSync , existsSync } from "node:fs"
6
+ import { join } from "node:path"
5
7
6
8
import { getSchedule , getSpeakers } from "@/app/conf/_api/sched-client"
9
+ import { SchedSpeaker , ScheduleSession } from "@/app/conf/_api/sched-types"
10
+ import { writeFile } from "node:fs/promises"
7
11
8
12
// Sched API rate limit is 30 requests per minute per token.
9
13
// This scripts fires:
@@ -23,7 +27,7 @@ const options = {
23
27
} ,
24
28
}
25
29
26
- function main ( ) {
30
+ async function main ( ) {
27
31
try {
28
32
const { values } = parseArgs ( { options } )
29
33
@@ -39,8 +43,7 @@ function main() {
39
43
const token = process . env [ `SCHED_ACCESS_TOKEN_${ year } ` ]
40
44
assert ( token , `SCHED_ACCESS_TOKEN_${ year } is not set` )
41
45
42
- // TODO: Implement sync logic here
43
- // You can now use the `year` variable in your sync logic
46
+ await sync ( year , token )
44
47
} catch ( error ) {
45
48
if ( error instanceof Error && error . message . includes ( "Unknown option" ) ) {
46
49
console . error ( `Error: ${ error . message } ` )
@@ -52,7 +55,20 @@ function main() {
52
55
}
53
56
54
57
if ( require . main === module ) {
55
- main ( )
58
+ void main ( )
59
+ }
60
+
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
+ }
56
72
}
57
73
58
74
async function sync ( year : number , token : string ) {
@@ -66,18 +82,159 @@ async function sync(year: number, token: string) {
66
82
67
83
const ctx = { apiUrl, token }
68
84
85
+ const scriptDir = __dirname
86
+ const speakersFilePath = join ( scriptDir , "speakers.json" )
87
+ const scheduleFilePath = join ( scriptDir , `schedule-${ year } .json` )
88
+
89
+ const existingSpeakers = readJsonFile < SchedSpeaker [ ] > ( speakersFilePath , [ ] )
90
+ const existingSchedule = readJsonFile < ScheduleSession [ ] > ( scheduleFilePath , [ ] )
91
+
69
92
console . log ( "Getting schedule and speakers list..." )
70
- const [ schedule , speakers ] = await Promise . all ( [
71
- getSchedule ( ctx ) ,
72
- getSpeakers ( ctx ) ,
73
- ] )
74
-
75
- // console.log("Getting speaker details...")
76
- // const speakerDetails = await Promise.all(
77
- // speakers.map(speaker => getSpeakerDetails(ctx, speaker.username)),
78
- // )
93
+
94
+ const schedule = getSchedule ( ctx )
95
+ const speakers = getSpeakers ( ctx )
96
+
97
+ const scheduleComparison = compare ( existingSchedule , await schedule , "id" )
98
+ printComparison ( scheduleComparison , "sessions" , "id" )
99
+
100
+ const writeSchedule = writeFile (
101
+ scheduleFilePath ,
102
+ JSON . stringify ( await schedule , null , 2 ) ,
103
+ )
104
+
105
+ const speakerComparison = compare (
106
+ existingSpeakers ,
107
+ await speakers ,
108
+ "username" ,
109
+ )
110
+ printComparison ( speakerComparison , "speakers" , "username" )
111
+
112
+ const updatedSpeakers = [
113
+ ...speakerComparison . removed ,
114
+ ...speakerComparison . unchanged ,
115
+ ...speakerComparison . changed . map ( change => change . new ) ,
116
+ ...speakerComparison . added . map ( speaker => ( {
117
+ ...speaker ,
118
+ [ "~syncedAt" ] : - 1 ,
119
+ } ) ) ,
120
+ ] . sort ( ( a , b ) => a . username . localeCompare ( b . username ) )
121
+
122
+ const writeSpeakers = writeFile (
123
+ speakersFilePath ,
124
+ JSON . stringify ( updatedSpeakers , null , 2 ) ,
125
+ )
126
+
127
+ writeSpeakers . then ( ( ) => {
128
+ console . log (
129
+ `Updated speakers data: ${ updatedSpeakers . length } total speakers` ,
130
+ )
131
+ } )
132
+
133
+ await writeSchedule
134
+ await writeSpeakers
79
135
}
80
136
81
137
function help ( ) {
82
138
return console . log ( "Usage: tsx sync.ts --year <year>" )
83
139
}
140
+
141
+ // #region utility
142
+
143
+ type Change < T > = { old : T ; new : T }
144
+ type Comparison < T > = {
145
+ added : T [ ]
146
+ removed : T [ ]
147
+ changed : Change < T > [ ]
148
+ unchanged : T [ ]
149
+ }
150
+
151
+ function compare < T extends object > ( olds : T [ ] , news : T [ ] , key : keyof T ) {
152
+ const oldMap = new Map ( olds . map ( o => [ o [ key ] , o ] ) )
153
+ const newMap = new Map ( news . map ( n => [ n [ key ] , n ] ) )
154
+
155
+ const added : T [ ] = [ ]
156
+ const removed : T [ ] = [ ]
157
+ const changed : Change < T > [ ] = [ ]
158
+ const unchanged : T [ ] = [ ]
159
+
160
+ for ( const newItem of news ) {
161
+ const oldItem = oldMap . get ( newItem [ key ] )
162
+ if ( oldItem ) {
163
+ if ( JSON . stringify ( oldItem ) === JSON . stringify ( newItem ) ) {
164
+ unchanged . push ( oldItem )
165
+ } else {
166
+ changed . push ( { old : oldItem , new : newItem } )
167
+ }
168
+ } else {
169
+ added . push ( newItem )
170
+ }
171
+ }
172
+
173
+ for ( const oldItem of olds ) {
174
+ if ( ! newMap . has ( oldItem [ key ] ) ) {
175
+ removed . push ( oldItem )
176
+ }
177
+ }
178
+
179
+ return { added, removed, changed, unchanged }
180
+ }
181
+
182
+ function printComparison < T extends object > (
183
+ comparison : Comparison < T > ,
184
+ name : string ,
185
+ key : keyof T ,
186
+ ) {
187
+ console . log ( `Removed: ${ comparison . removed . length } ` )
188
+ console . log ( `Changed: ${ comparison . changed . length } ` )
189
+ console . log ( `Unchanged: ${ comparison . unchanged . length } ` )
190
+
191
+ console . log ( `Added ${ comparison . added . length } ${ name } ` )
192
+ for ( const item of comparison . added ) {
193
+ console . log ( `+ ${ item } ` )
194
+ }
195
+
196
+ console . log ( `Removed ${ comparison . removed . length } ${ name } ` )
197
+ for ( const item of comparison . removed ) {
198
+ console . log ( `- ${ item } ` )
199
+ }
200
+
201
+ console . log ( `Unchanged ${ comparison . unchanged . length } ${ name } ` )
202
+ for ( const item of comparison . unchanged ) {
203
+ console . log ( item )
204
+ }
205
+
206
+ console . log ( `Changed ${ comparison . changed . length } ${ name } ` )
207
+ for ( const change of comparison . changed ) {
208
+ console . log ( change . new [ key ] , objectDiff ( change ) )
209
+ }
210
+ }
211
+
212
+ function objectDiff < T extends object > ( change : Change < T > ) : string {
213
+ const allKeys = [
214
+ ...new Set ( [
215
+ ...( Object . keys ( change . old ) as Array < keyof T > ) ,
216
+ ...( Object . keys ( change . new ) as Array < keyof T > ) ,
217
+ ] ) ,
218
+ ] . sort ( )
219
+
220
+ const diff = allKeys
221
+ . map ( key => {
222
+ const oldValue = change . old [ key ]
223
+ const newValue = change . new [ key ]
224
+
225
+ if ( oldValue === newValue ) {
226
+ return null
227
+ }
228
+
229
+ return { key, oldValue, newValue }
230
+ } )
231
+ . filter ( x => ! ! x )
232
+
233
+ return diff
234
+ . map ( diff => {
235
+ return `\x1b[33m${ String ( diff . key ) } \x1b[0m: ${ diff . oldValue } -> ${ diff . newValue } `
236
+ } )
237
+ . join ( "\n" )
238
+ }
239
+
240
+ // #endregion utility
0 commit comments