@rharkor/caching-for-turbo 2.0.0 → 2.1.1
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.
- package/.env.example +6 -1
- package/README.md +14 -46
- package/dist/cli/index.js +66 -40
- package/dist/cli/index.js.map +1 -1
- package/package.json +2 -62
- package/.devcontainer/devcontainer.json +0 -41
- package/.gitattributes +0 -3
- package/.github/workflows/check-dist.yml +0 -43
- package/.github/workflows/ci.yml +0 -152
- package/.github/workflows/codeql-analysis.yml +0 -46
- package/.github/workflows/release.yml +0 -47
- package/.node-version +0 -1
- package/.prettierignore +0 -3
- package/.prettierrc.json +0 -16
- package/.releaserc.json +0 -132
- package/CHANGELOG.md +0 -9
- package/check-full-turbo.sh +0 -12
- package/eslint.config.mjs +0 -27
- package/renovate.json +0 -17
- package/script/release +0 -59
- package/src/cli.ts +0 -94
- package/src/dev/cleanup.ts +0 -14
- package/src/dev-run.ts +0 -16
- package/src/index.ts +0 -6
- package/src/lib/constants.ts +0 -29
- package/src/lib/core.ts +0 -62
- package/src/lib/env/index.ts +0 -22
- package/src/lib/ping.ts +0 -7
- package/src/lib/providers/cache/index.ts +0 -87
- package/src/lib/providers/cache/utils.ts +0 -70
- package/src/lib/providers/s3/index.ts +0 -237
- package/src/lib/providers.ts +0 -42
- package/src/lib/server/cleanup.ts +0 -111
- package/src/lib/server/index.ts +0 -85
- package/src/lib/server/utils.ts +0 -91
- package/src/main.ts +0 -21
- package/src/post.ts +0 -26
- package/tsconfig.json +0 -19
- package/turbo.json +0 -9
package/script/release
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
# About:
|
|
4
|
-
#
|
|
5
|
-
# This is a helper script to tag and push a new release. GitHub Actions use
|
|
6
|
-
# release tags to allow users to select a specific version of the action to use.
|
|
7
|
-
#
|
|
8
|
-
# See: https://github.com/actions/typescript-action#publishing-a-new-release
|
|
9
|
-
#
|
|
10
|
-
# This script will do the following:
|
|
11
|
-
#
|
|
12
|
-
# 1. Get the latest release tag
|
|
13
|
-
# 2. Prompt the user for a new release tag
|
|
14
|
-
# 3. Tag the new release
|
|
15
|
-
# 4. Push the new tag to the remote
|
|
16
|
-
#
|
|
17
|
-
# Usage:
|
|
18
|
-
#
|
|
19
|
-
# script/release
|
|
20
|
-
|
|
21
|
-
# Terminal colors
|
|
22
|
-
OFF='\033[0m'
|
|
23
|
-
RED='\033[0;31m'
|
|
24
|
-
GREEN='\033[0;32m'
|
|
25
|
-
BLUE='\033[0;34m'
|
|
26
|
-
|
|
27
|
-
# Get the latest release tag
|
|
28
|
-
latest_tag=$(git describe --tags "$(git rev-list --tags --max-count=1)")
|
|
29
|
-
|
|
30
|
-
if [[ -z "$latest_tag" ]]; then
|
|
31
|
-
# There are no existing release tags
|
|
32
|
-
echo -e "No tags found (yet) - Continue to create and push your first tag"
|
|
33
|
-
latest_tag="[unknown]"
|
|
34
|
-
fi
|
|
35
|
-
|
|
36
|
-
# Display the latest release tag
|
|
37
|
-
echo -e "The latest release tag is: ${BLUE}${latest_tag}${OFF}"
|
|
38
|
-
|
|
39
|
-
# Prompt the user for the new release tag
|
|
40
|
-
read -r -p 'Enter a new release tag (vX.X.X format): ' new_tag
|
|
41
|
-
|
|
42
|
-
# Validate the new release tag
|
|
43
|
-
tag_regex='v[0-9]+\.[0-9]+\.[0-9]+$'
|
|
44
|
-
if echo "$new_tag" | grep -q -E "$tag_regex"; then
|
|
45
|
-
echo -e "Tag: ${BLUE}$new_tag${OFF} is valid"
|
|
46
|
-
else
|
|
47
|
-
# Release tag is not `vX.X.X` format
|
|
48
|
-
echo -e "Tag: ${BLUE}$new_tag${OFF} is ${RED}not valid${OFF} (must be in vX.X.X format)"
|
|
49
|
-
exit 1
|
|
50
|
-
fi
|
|
51
|
-
|
|
52
|
-
# Tag the new release
|
|
53
|
-
git tag -a "$new_tag" -m "$new_tag Release"
|
|
54
|
-
echo -e "${GREEN}Tagged: $new_tag${OFF}"
|
|
55
|
-
|
|
56
|
-
# Push the new tag to the remote
|
|
57
|
-
git push --tags
|
|
58
|
-
echo -e "${GREEN}Release tag pushed to remote${OFF}"
|
|
59
|
-
echo -e "${GREEN}Done!${OFF}"
|
package/src/cli.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { config } from 'dotenv'
|
|
4
|
-
import { server } from './lib/server'
|
|
5
|
-
import { killServer, launchServer } from './lib/server/utils'
|
|
6
|
-
import { logger } from '@rharkor/logger'
|
|
7
|
-
import { ping } from './lib/ping'
|
|
8
|
-
import { serverLogFile } from './lib/constants'
|
|
9
|
-
import { writeFile } from 'fs/promises'
|
|
10
|
-
import { version } from '../package.json'
|
|
11
|
-
|
|
12
|
-
// Load environment variables from .env file if it exists
|
|
13
|
-
config()
|
|
14
|
-
|
|
15
|
-
const main = async (): Promise<void> => {
|
|
16
|
-
await logger.init()
|
|
17
|
-
|
|
18
|
-
const args = process.argv.slice(2)
|
|
19
|
-
|
|
20
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
21
|
-
console.log(`
|
|
22
|
-
Turborepo GitHub Actions Cache Server
|
|
23
|
-
|
|
24
|
-
Usage:
|
|
25
|
-
turbogha [options]
|
|
26
|
-
|
|
27
|
-
Options:
|
|
28
|
-
--server Run the server in foreground mode
|
|
29
|
-
--help, -h Show this help message
|
|
30
|
-
--version, -v Show version
|
|
31
|
-
--ping Ping the server
|
|
32
|
-
--kill Kill the server
|
|
33
|
-
|
|
34
|
-
Environment Variables:
|
|
35
|
-
The following environment variables are supported for S3 configuration:
|
|
36
|
-
- S3_ACCESS_KEY_ID: AWS S3 access key ID
|
|
37
|
-
- S3_SECRET_ACCESS_KEY: AWS S3 secret access key
|
|
38
|
-
- S3_BUCKET: AWS S3 bucket name
|
|
39
|
-
- S3_REGION: AWS S3 region
|
|
40
|
-
- S3_ENDPOINT: S3 endpoint (default: https://s3.amazonaws.com)
|
|
41
|
-
- S3_PREFIX: Prefix for S3 objects (default: turbogha/)
|
|
42
|
-
- PROVIDER: Cache provider (github or s3)
|
|
43
|
-
|
|
44
|
-
Examples:
|
|
45
|
-
# Run server in background and export environment variables
|
|
46
|
-
turbogha
|
|
47
|
-
|
|
48
|
-
# Run server in foreground mode
|
|
49
|
-
turbogha --server
|
|
50
|
-
|
|
51
|
-
# Ping the server
|
|
52
|
-
turbogha --ping
|
|
53
|
-
|
|
54
|
-
# With S3 configuration
|
|
55
|
-
S3_ACCESS_KEY_ID=your-key S3_SECRET_ACCESS_KEY=your-secret S3_BUCKET=your-bucket S3_REGION=us-east-1 turbogha
|
|
56
|
-
`)
|
|
57
|
-
process.exit(0)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (args.includes('--version') || args.includes('-v')) {
|
|
61
|
-
console.log(version)
|
|
62
|
-
process.exit(0)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
if (args.includes('--ping')) {
|
|
67
|
-
await ping()
|
|
68
|
-
} else if (args.includes('--kill')) {
|
|
69
|
-
await killServer()
|
|
70
|
-
} else if (args.includes('--server')) {
|
|
71
|
-
// Empty log file
|
|
72
|
-
await writeFile(serverLogFile, '', { flag: 'w' })
|
|
73
|
-
// Run server in foreground mode
|
|
74
|
-
console.log('Starting Turborepo cache server in foreground mode...')
|
|
75
|
-
await server()
|
|
76
|
-
} else {
|
|
77
|
-
// Run server in background mode and export environment variables
|
|
78
|
-
console.log('Starting Turborepo cache server...')
|
|
79
|
-
// Empty log file
|
|
80
|
-
await writeFile(serverLogFile, '', { flag: 'w' })
|
|
81
|
-
await launchServer()
|
|
82
|
-
console.log(
|
|
83
|
-
'\nServer is running! You can now use Turbo with remote caching.'
|
|
84
|
-
)
|
|
85
|
-
console.log('\nTo stop the server, run:')
|
|
86
|
-
console.log('curl -X DELETE http://localhost:41230/shutdown')
|
|
87
|
-
}
|
|
88
|
-
} catch (error) {
|
|
89
|
-
console.error('Error:', error)
|
|
90
|
-
process.exit(1)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
main()
|
package/src/dev/cleanup.ts
DELETED
package/src/dev-run.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// Run the server in foreground and kill it after the test
|
|
2
|
-
|
|
3
|
-
import { config } from 'dotenv'
|
|
4
|
-
config()
|
|
5
|
-
|
|
6
|
-
import { server } from './lib/server'
|
|
7
|
-
import { launchServer } from './lib/server/utils'
|
|
8
|
-
|
|
9
|
-
const main = async (): Promise<void> => {
|
|
10
|
-
//* Run server
|
|
11
|
-
server()
|
|
12
|
-
//* Run launch server
|
|
13
|
-
await launchServer(true)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
main()
|
package/src/index.ts
DELETED
package/src/lib/constants.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { join } from 'path'
|
|
2
|
-
import { env } from './env'
|
|
3
|
-
import { core } from './core'
|
|
4
|
-
|
|
5
|
-
// Helper function to get input value, prioritizing environment variables for local development
|
|
6
|
-
const getInput = (name: string, envName?: string): string | undefined => {
|
|
7
|
-
// In GitHub Actions context, try core.getInput first
|
|
8
|
-
if (process.env.CI === 'true') {
|
|
9
|
-
const coreInput = core.getInput(name)
|
|
10
|
-
if (coreInput) return coreInput
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Fall back to environment variable
|
|
14
|
-
const envVar = envName || name.toUpperCase().replace(/-/g, '_')
|
|
15
|
-
return process.env[envVar]
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const serverPort = 41230
|
|
19
|
-
export const cachePath = 'turbogha_'
|
|
20
|
-
export const cachePrefix = getInput('cache-prefix', 'CACHE_PREFIX') || cachePath
|
|
21
|
-
export const getCacheKey = (hash: string, tag?: string): string =>
|
|
22
|
-
`${cachePrefix}${hash}${tag ? `#${tag}` : ''}`
|
|
23
|
-
export const serverLogFile = env.RUNNER_TEMP
|
|
24
|
-
? join(env.RUNNER_TEMP, 'turbogha.log')
|
|
25
|
-
: '/tmp/turbogha.log'
|
|
26
|
-
export const getFsCachePath = (hash: string): string =>
|
|
27
|
-
join(env.RUNNER_TEMP || '/tmp', `${hash}.tg.bin`)
|
|
28
|
-
export const getTempCachePath = (key: string): string =>
|
|
29
|
-
join(env.RUNNER_TEMP || '/tmp', `cache-${key}.tg.bin`)
|
package/src/lib/core.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import * as coreLib from '@actions/core'
|
|
2
|
-
import { logger as loggerLib } from '@rharkor/logger'
|
|
3
|
-
|
|
4
|
-
const isCI = process.env.CI === 'true'
|
|
5
|
-
|
|
6
|
-
export const core = {
|
|
7
|
-
isCI,
|
|
8
|
-
setFailed: (message: string) => {
|
|
9
|
-
if (isCI) {
|
|
10
|
-
coreLib.setFailed(message)
|
|
11
|
-
} else {
|
|
12
|
-
loggerLib.error(message)
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
getInput: (name: string) => {
|
|
16
|
-
if (isCI) {
|
|
17
|
-
return coreLib.getInput(name)
|
|
18
|
-
}
|
|
19
|
-
return undefined
|
|
20
|
-
},
|
|
21
|
-
exportVariable: (name: string, value: string) => {
|
|
22
|
-
if (isCI) {
|
|
23
|
-
coreLib.exportVariable(name, value)
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
//* Logger
|
|
27
|
-
info: (message: string) => {
|
|
28
|
-
if (isCI) {
|
|
29
|
-
coreLib.info(message)
|
|
30
|
-
} else {
|
|
31
|
-
loggerLib.info(message)
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
error: (message: string) => {
|
|
35
|
-
if (isCI) {
|
|
36
|
-
coreLib.error(message)
|
|
37
|
-
} else {
|
|
38
|
-
loggerLib.error(message)
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
debug: (message: string) => {
|
|
42
|
-
if (isCI) {
|
|
43
|
-
coreLib.debug(message)
|
|
44
|
-
} else {
|
|
45
|
-
loggerLib.debug(message)
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
log: (message: string) => {
|
|
49
|
-
if (isCI) {
|
|
50
|
-
coreLib.info(message)
|
|
51
|
-
} else {
|
|
52
|
-
loggerLib.log(message)
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
success: (message: string) => {
|
|
56
|
-
if (isCI) {
|
|
57
|
-
coreLib.info(message)
|
|
58
|
-
} else {
|
|
59
|
-
loggerLib.success(message)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
package/src/lib/env/index.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
const envObject = {
|
|
2
|
-
ACTIONS_RUNTIME_TOKEN: process.env.ACTIONS_RUNTIME_TOKEN,
|
|
3
|
-
ACTIONS_CACHE_URL: process.env.ACTIONS_CACHE_URL,
|
|
4
|
-
RUNNER_TEMP: process.env.RUNNER_TEMP
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
type TInvalidEnv = {
|
|
8
|
-
valid: false
|
|
9
|
-
} & typeof envObject
|
|
10
|
-
|
|
11
|
-
type TValidEnv = {
|
|
12
|
-
valid: true
|
|
13
|
-
} & {
|
|
14
|
-
[K in keyof typeof envObject]: NonNullable<(typeof envObject)[K]>
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type TEnv = TInvalidEnv | TValidEnv
|
|
18
|
-
|
|
19
|
-
export const env = {
|
|
20
|
-
valid: Object.values(envObject).every(value => value !== undefined),
|
|
21
|
-
...envObject
|
|
22
|
-
} as TEnv
|
package/src/lib/ping.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'node:stream'
|
|
2
|
-
import { env } from '../../env'
|
|
3
|
-
import { pipeline } from 'node:stream/promises'
|
|
4
|
-
import {
|
|
5
|
-
createReadStream,
|
|
6
|
-
createWriteStream,
|
|
7
|
-
existsSync,
|
|
8
|
-
statSync
|
|
9
|
-
} from 'node:fs'
|
|
10
|
-
import { getCacheKey, getFsCachePath, getTempCachePath } from '../../constants'
|
|
11
|
-
import { RequestContext } from '../../server'
|
|
12
|
-
import { TListFile } from '../../server/cleanup'
|
|
13
|
-
import { getCacheClient } from './utils'
|
|
14
|
-
import { TProvider } from '../../providers'
|
|
15
|
-
import { core } from 'src/lib/core'
|
|
16
|
-
|
|
17
|
-
//* Cache API
|
|
18
|
-
export async function saveCache(
|
|
19
|
-
ctx: RequestContext,
|
|
20
|
-
hash: string,
|
|
21
|
-
tag: string,
|
|
22
|
-
stream: Readable
|
|
23
|
-
): Promise<void> {
|
|
24
|
-
if (!env.valid) {
|
|
25
|
-
ctx.log.info(
|
|
26
|
-
`Using filesystem cache because cache API env vars are not set`
|
|
27
|
-
)
|
|
28
|
-
await pipeline(stream, createWriteStream(getFsCachePath(hash)))
|
|
29
|
-
return
|
|
30
|
-
}
|
|
31
|
-
const client = getCacheClient()
|
|
32
|
-
const key = getCacheKey(hash, tag)
|
|
33
|
-
await client.save(key, stream)
|
|
34
|
-
ctx.log.info(`Saved cache ${key} for ${hash}`)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function getCache(
|
|
38
|
-
ctx: RequestContext,
|
|
39
|
-
hash: string
|
|
40
|
-
): Promise<
|
|
41
|
-
[number | undefined, Readable | ReadableStream, string | undefined] | null
|
|
42
|
-
> {
|
|
43
|
-
//* Get cache from filesystem if cache API env vars are not set
|
|
44
|
-
if (!env.valid) {
|
|
45
|
-
const path = getFsCachePath(hash)
|
|
46
|
-
if (!existsSync(path)) return null
|
|
47
|
-
const size = statSync(path).size
|
|
48
|
-
return [size, createReadStream(path), undefined]
|
|
49
|
-
}
|
|
50
|
-
//* Get cache from cache API
|
|
51
|
-
const client = getCacheClient()
|
|
52
|
-
const cacheKey = getCacheKey(hash)
|
|
53
|
-
const fileRestorationPath = getTempCachePath(cacheKey)
|
|
54
|
-
const foundKey = await client.restore(fileRestorationPath, cacheKey)
|
|
55
|
-
ctx.log.info(`Cache lookup for ${cacheKey}`)
|
|
56
|
-
if (!foundKey) {
|
|
57
|
-
ctx.log.info(`Cache lookup did not return data`)
|
|
58
|
-
return null
|
|
59
|
-
}
|
|
60
|
-
const [foundCacheKey, artifactTag] = String(foundKey).split('#')
|
|
61
|
-
if (foundCacheKey !== cacheKey) {
|
|
62
|
-
ctx.log.info(`Cache key mismatch: ${foundCacheKey} !== ${cacheKey}`)
|
|
63
|
-
return null
|
|
64
|
-
}
|
|
65
|
-
const size = statSync(fileRestorationPath).size
|
|
66
|
-
const readableStream = createReadStream(fileRestorationPath)
|
|
67
|
-
return [size, readableStream, artifactTag]
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function deleteCache(): Promise<void> {
|
|
71
|
-
core.error(`Cannot delete github cache automatically.`)
|
|
72
|
-
throw new Error(`Cannot delete github cache automatically.`)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export async function listCache(): Promise<TListFile[]> {
|
|
76
|
-
core.error(`Cannot list github cache automatically.`)
|
|
77
|
-
throw new Error(`Cannot list github cache automatically.`)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export const getGithubProvider = (): TProvider => {
|
|
81
|
-
return {
|
|
82
|
-
save: saveCache,
|
|
83
|
-
get: getCache,
|
|
84
|
-
delete: deleteCache,
|
|
85
|
-
list: listCache
|
|
86
|
-
}
|
|
87
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'node:stream'
|
|
2
|
-
import { env } from '../../env'
|
|
3
|
-
import streamToPromise from 'stream-to-promise'
|
|
4
|
-
import { createWriteStream } from 'node:fs'
|
|
5
|
-
import { unlink } from 'node:fs/promises'
|
|
6
|
-
import { getTempCachePath } from '../../constants'
|
|
7
|
-
import { restoreCache, saveCache } from '@actions/cache'
|
|
8
|
-
import { core } from 'src/lib/core'
|
|
9
|
-
class HandledError extends Error {
|
|
10
|
-
status: number
|
|
11
|
-
statusText: string
|
|
12
|
-
data: unknown
|
|
13
|
-
constructor(status: number, statusText: string, data: unknown) {
|
|
14
|
-
super(`${status}: ${statusText}`)
|
|
15
|
-
this.status = status
|
|
16
|
-
this.statusText = statusText
|
|
17
|
-
this.data = data
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function handleFetchError(message: string) {
|
|
22
|
-
return (error: unknown) => {
|
|
23
|
-
if (error instanceof HandledError) {
|
|
24
|
-
core.error(`${message}: ${error.status} ${error.statusText}`)
|
|
25
|
-
core.error(JSON.stringify(error.data))
|
|
26
|
-
throw error
|
|
27
|
-
}
|
|
28
|
-
core.error(`${message}: ${error}`)
|
|
29
|
-
throw error
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function getCacheClient() {
|
|
34
|
-
if (!env.valid) {
|
|
35
|
-
throw new Error('Cache API env vars are not set')
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const save = async (key: string, stream: Readable): Promise<void> => {
|
|
39
|
-
try {
|
|
40
|
-
//* Create a temporary file to store the cache
|
|
41
|
-
const tempFile = getTempCachePath(key)
|
|
42
|
-
const writeStream = createWriteStream(tempFile)
|
|
43
|
-
await streamToPromise(stream.pipe(writeStream))
|
|
44
|
-
core.info(`Saved cache to ${tempFile}`)
|
|
45
|
-
|
|
46
|
-
core.info(`Saving cache for key: ${key}, path: ${tempFile}`)
|
|
47
|
-
await saveCache([tempFile], key)
|
|
48
|
-
core.info(`Saved cache ${key}`)
|
|
49
|
-
|
|
50
|
-
//* Remove the temporary file
|
|
51
|
-
await unlink(tempFile)
|
|
52
|
-
} catch (error) {
|
|
53
|
-
handleFetchError('Unable to upload cache')(error)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const restore = async (
|
|
58
|
-
path: string,
|
|
59
|
-
key: string
|
|
60
|
-
): Promise<string | undefined> => {
|
|
61
|
-
core.info(`Querying cache for key: ${key}, path: ${path}`)
|
|
62
|
-
|
|
63
|
-
return restoreCache([path], key, [])
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
save,
|
|
68
|
-
restore
|
|
69
|
-
}
|
|
70
|
-
}
|