Skip to content

Commit 588b60b

Browse files
committed
Merge branch 'qr' of github.com:sheepzh/timer into qr
2 parents 1ffb905 + 2524055 commit 588b60b

File tree

4 files changed

+160
-40
lines changed

4 files changed

+160
-40
lines changed

src/api/http.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,20 @@ export async function fetchPost<T>(url: string, body?: T, option?: Option): Prom
3232
}
3333
}
3434

35+
export async function fetchPut<T>(url: string, body?: T, option?: Option): Promise<Response> {
36+
try {
37+
const response = await fetch(url, {
38+
...(option || {}),
39+
method: "PUT",
40+
body: body ? JSON.stringify(body) : null,
41+
})
42+
return response
43+
} catch (e) {
44+
console.error("Failed to fetch post", e)
45+
throw Error(e)
46+
}
47+
}
48+
3549
export async function fetchPutText(url: string, bodyText?: string, option?: Option): Promise<Response> {
3650
try {
3751
const response = await fetch(url, {

src/api/quantified-resume.ts

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fetchGet, fetchPost } from "./http"
1+
import { fetchDelete, fetchGet, fetchPost, fetchPut } from "./http"
22

33
const QR_BUILTIN_TYPE = "BrowserTime"
44
export const DEFAULT_ENDPOINT = "http://localhost:12233"
@@ -18,32 +18,81 @@ export type Bucket = {
1818
payload?: BucketPayload
1919
}
2020

21+
export type Item = {
22+
refId: string
23+
timestamp: number
24+
name?: string
25+
action: string
26+
metrics: {
27+
visit: number
28+
focus: number
29+
}
30+
payload?: Record<string, any>
31+
}
32+
2133
export type QrRequestContext = {
2234
endpoint?: string
2335
}
2436

37+
const headers = (): Headers => {
38+
const headers = new Headers()
39+
headers.append("Content-Type", "application/json")
40+
return headers
41+
}
42+
2543
export const listBuckets = async (ctx: QrRequestContext, clientId?: string): Promise<Bucket[]> => {
2644
const response = await fetchGet(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket?bt=${QR_BUILTIN_TYPE}&bt_rid=${clientId || ''}`)
27-
const status = response?.status
28-
if (status === 200) {
29-
return await response.json()
30-
} else if (status === 422) {
31-
throw new Error("Failed to connect Quantified Resume, please contact the developer")
32-
} else {
33-
console.error(response)
34-
throw new Error("Unexpected status code: " + status)
35-
}
45+
return handleResponseJson(response)
3646
}
3747

3848
export const createBucket = async (ctx: QrRequestContext, bucket: Bucket): Promise<number> => {
39-
const response = await fetchPost(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket`, bucket)
49+
const response = await fetchPost(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket`, bucket, { headers: headers() })
50+
return handleResponseJson(response)
51+
}
52+
53+
async function handleResponseJson<T>(response: Response): Promise<T> {
54+
await handleResponse(response)
55+
return response.json()
56+
}
57+
58+
async function handleResponse(response: Response): Promise<Response> {
4059
const status = response?.status
4160
if (status === 200) {
42-
return await response.json()
43-
} else if (status === 422) {
61+
return
62+
}
63+
let resMsg = null
64+
try {
65+
resMsg = (await response.json()).message
66+
} catch { }
67+
if (resMsg) {
68+
throw new Error(resMsg)
69+
}
70+
if (status === 422) {
4471
throw new Error("Failed to connect Quantified Resume, please contact the developer")
72+
} else if (status === 500) {
73+
throw new Error("Internal server error")
4574
} else {
4675
console.error(response)
47-
throw new Error("Unexpected status code: " + status)
76+
throw new Error(`Unexpected status code: ${status}, url=${response.url}`)
4877
}
78+
}
79+
80+
export const updateBucket = async (ctx: QrRequestContext, bucket: Bucket): Promise<void> => {
81+
const response = await fetchPut(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket/${bucket?.id}`, bucket, { headers: headers() })
82+
await handleResponse(response)
83+
}
84+
85+
export const batchCreateItems = async (ctx: QrRequestContext, bucketId: number, items: Item[]): Promise<void> => {
86+
const response = await fetchPost(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket/${bucketId}/item`, items, { headers: headers() })
87+
await handleResponse(response)
88+
}
89+
90+
export const listAllItems = async (ctx: QrRequestContext, bucketId: number): Promise<Item[]> => {
91+
const response = await fetchGet(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket/${bucketId}/item`, { headers: headers() })
92+
return await handleResponseJson(response)
93+
}
94+
95+
export const removeBucket = async (ctx: QrRequestContext, bucketId: number): Promise<void> => {
96+
const response = await fetchDelete(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket/${bucketId}?force=true`, { headers: headers() })
97+
await handleResponse(response)
4998
}
Lines changed: 80 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { Bucket, listBuckets } from "@api/quantified-resume"
1+
import { batchCreateItems, Bucket, createBucket, Item, listAllItems, listBuckets, removeBucket, updateBucket } from "@api/quantified-resume"
2+
import metaMessages, { } from "@i18n/message/common/meta"
3+
import { t } from "@i18n"
4+
import { groupBy } from "@util/array"
5+
import { formatTimeYMD, parseTime } from "@util/time"
26

37
export type QuantifiedResumeCache = {
48
bucketIds: {
@@ -7,34 +11,54 @@ export type QuantifiedResumeCache = {
711
}
812
}
913

10-
async function getBucketId(context: timer.backup.CoordinatorContext<QuantifiedResumeCache>): Promise<number> {
11-
const { cid, cache } = context || {}
14+
async function createNewBucket(context: timer.backup.CoordinatorContext<QuantifiedResumeCache>): Promise<number> {
15+
const { cid, cname } = context || {}
16+
const { endpoint } = context?.ext || {}
17+
const appName = t(metaMessages, { key: msg => msg.name })
18+
const bucket: Bucket = {
19+
name: `${appName}: ${cid}`,
20+
builtin: "BrowserTime",
21+
builtinRefId: cid,
22+
payload: { name: cname }
23+
}
24+
return createBucket({ endpoint }, bucket)
25+
}
26+
27+
async function getBucketId(context: timer.backup.CoordinatorContext<QuantifiedResumeCache>, specificCid?: string): Promise<number> {
28+
const cid = specificCid || context?.cid
29+
const { cache } = context || {}
1230
// 1. query from cache
1331
let bucketId = cache?.bucketIds?.[cid]
14-
if (!bucketId) return bucketId
32+
if (bucketId) return bucketId
1533

1634
const { endpoint } = context?.ext || {}
1735
// 2. query again
18-
bucketId = (await listBuckets({ endpoint }, cid))?.[0]?.id
19-
// TODO
36+
bucketId = (await listBuckets({ endpoint }, cid))?.filter(b => b.builtinRefId === cid)?.[0]?.id
2037
if (!bucketId) {
2138
// 3. create one
22-
const bucket: Bucket = {
23-
name: "Time Tracker: " + cid,
24-
builtin: "BrowserTime",
25-
builtinRefId: cid,
26-
payload: {
27-
name: ""
28-
}
29-
}
39+
bucketId = await createNewBucket(context)
3040
}
31-
throw new Error("TODO")
41+
return bucketId
3242
}
3343

3444
export default class QuantifiedResumeCoordinator implements timer.backup.Coordinator<QuantifiedResumeCache> {
35-
36-
updateClients(_: timer.backup.CoordinatorContext<QuantifiedResumeCache>, clients: timer.backup.Client[]): Promise<void> {
37-
throw new Error("Method not implemented.");
45+
async updateClients(context: timer.backup.CoordinatorContext<QuantifiedResumeCache>, clients: timer.backup.Client[]): Promise<void> {
46+
const { endpoint } = context?.ext || {}
47+
const existBuckets = groupBy(await listBuckets({ endpoint }) || [], b => b.builtinRefId, l => l?.[0])
48+
if (!clients?.length) return
49+
const promises = Promise.all(clients.map(
50+
async ({ id, name, minDate, maxDate }) => {
51+
const exist = existBuckets[id]
52+
if (exist) {
53+
// update payload
54+
exist.payload = { name, minDate, maxDate }
55+
await updateBucket({ endpoint }, exist)
56+
} else {
57+
await createNewBucket(context)
58+
}
59+
})
60+
)
61+
await promises
3862
}
3963

4064
async listAllClients(context: timer.backup.CoordinatorContext<QuantifiedResumeCache>): Promise<timer.backup.Client[]> {
@@ -60,13 +84,44 @@ export default class QuantifiedResumeCoordinator implements timer.backup.Coordin
6084
return result
6185
}
6286

63-
download(context: timer.backup.CoordinatorContext<QuantifiedResumeCache>, dateStart: Date, dateEnd: Date, targetCid?: string): Promise<timer.stat.RowBase[]> {
64-
throw new Error("Method not implemented.");
87+
async download(context: timer.backup.CoordinatorContext<QuantifiedResumeCache>, dateStart: Date, dateEnd: Date, targetCid?: string): Promise<timer.stat.RowBase[]> {
88+
let bucketId = await getBucketId(context, targetCid)
89+
if (!bucketId) return []
90+
const items = await listAllItems({ endpoint: context?.ext?.endpoint }, bucketId)
91+
return items?.map(({ name, timestamp, metrics }) => ({
92+
host: name,
93+
date: formatTimeYMD(timestamp),
94+
focus: metrics?.focus,
95+
time: metrics?.visit,
96+
} satisfies timer.stat.RowBase)) || []
6597
}
6698

6799
async upload(context: timer.backup.CoordinatorContext<QuantifiedResumeCache>, rows: timer.stat.RowBase[]): Promise<void> {
100+
if (!rows?.length) return
101+
68102
const bucketId = await getBucketId(context)
69-
throw new Error("Method not implemented.");
103+
let items = rows.map(({ host, date, focus, time: visit }) => {
104+
const time = parseTime(date)
105+
time.setHours(0)
106+
time.setMinutes(0)
107+
time.setSeconds(0)
108+
time.setMilliseconds(0)
109+
const item: Item = {
110+
refId: `${date}${host}`,
111+
timestamp: time.getTime(),
112+
metrics: { visit, focus },
113+
action: "web_time",
114+
name: host,
115+
payload: { date, host, cid: context.cid },
116+
}
117+
return item
118+
})
119+
const groups = groupBy(items, (_, idx) => Math.floor(idx / 2000), l => l)
120+
121+
const { endpoint } = context?.ext || {}
122+
for (const group of Object.values(groups)) {
123+
await batchCreateItems({ endpoint }, bucketId, group)
124+
}
70125
}
71126

72127
async testAuth(_auth: timer.backup.Auth, ext: timer.backup.TypeExt): Promise<string> {
@@ -78,7 +133,9 @@ export default class QuantifiedResumeCoordinator implements timer.backup.Coordin
78133
}
79134
}
80135

81-
clear(context: timer.backup.CoordinatorContext<QuantifiedResumeCache>, client: timer.backup.Client): Promise<void> {
82-
throw new Error("Method not implemented.");
136+
async clear(context: timer.backup.CoordinatorContext<QuantifiedResumeCache>, client: timer.backup.Client): Promise<void> {
137+
const bucketId = await getBucketId(context, client.id)
138+
if (!bucketId) return
139+
await removeBucket({ endpoint: context?.ext?.endpoint }, bucketId)
83140
}
84141
}

src/util/array.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
*/
1616
export function groupBy<T, R>(
1717
arr: T[],
18-
keyFunc: (e: T) => string | number,
18+
keyFunc: (e: T, idx: number) => string | number,
1919
downstream: (grouped: T[], key: string) => R
2020
): { [key: string]: R } {
2121
const groupedMap: { [key: string]: T[] } = {}
22-
arr.forEach(e => {
23-
const key = keyFunc(e)
22+
arr.forEach((e, idx) => {
23+
const key = keyFunc(e, idx)
2424
if (key === undefined || key === null) {
2525
return
2626
}

0 commit comments

Comments
 (0)