An abstraction for loading imposters from the filesystem
The root of the database is provided on the command line as --datadir
The file layout looks like this:
/{datadir}
/3000
/imposter.json
{
"protocol": "http",
"port": 3000,
"stubs: [{
"predicates": [{ "equals": { "path": "/" } }],
"meta": {
"dir": "stubs/{epoch-pid-counter}"
}
}]
}
/stubs
/{epoch-pid-counter}
/meta.json
{
"responseFiles": ["responses/{epoch-pid-counter}.json"],
// An array of indexes into responseFiles which handle repeat behavior
"orderWithRepeats": [0],
// The next index into orderWithRepeats; incremented with each call to nextResponse()
"nextIndex": 0
}
/responses
/{epoch-pid-counter}.json
{
"is": { "body": "Hello, world!" }
}
/matches
/{epoch-pid-counter}.json
{ ... }
/requests
/{epoch-pid-counter}.json
{ ... }
This structure is designed to minimize the amount of file locking and to maximize parallelism and throughput.
The imposters.json file needs to be locked during imposter-level activities (e.g. adding a stub).
Readers do not lock; they just get the data at the time they read it. Since this file doesn't
contain transient state, there's no harm from a stale read. Writes (which happen for
stub changing operations, either for proxy response recording or stub API calls) grab a file lock
for both the read and the write. Writes to this file should be infrequent, except perhaps during
proxy recording. Newly added stubs may change the index of existing stubs in the stubs array, but
will never change the stub meta.dir, so it is always safe to hang on to it for subsequent operations.
The stub meta.json needs to be locked to add responses or trigger the next response, but is
separated from the imposter.json so we can have responses from multiple stubs in parallel with no
lock conflict. Again, readers (e.g. to generate imposter JSON) do not need a lock because the responseFiles
array is mostly read-only, and even when it's not (adding responses during proxyAlways recording), there's
no harm from a stale read. Writers (usually generating the next response for a stub) grab a lock for the
read and the write. This should be the most common write across files, which is why the meta.json file
is small.
In both cases where a file needs to be locked, an exponential backoff retry strategy is used. Inconsistent
reads of partially written files (which can happen by default with the system calls - fs.writeFile is not
atomic) are avoided by writing first to a temp file (during which time reads can happen to the original file)
and then renaming to the original file.
New directories and filenames use a timestamp-based filename to allow creating them without synchronizing
with a read. Since multiple files (esp. requests) can be added during the same millisecond, a pid and counter
is tacked on to the filename to improve uniqueness. It doesn't provide the * ironclad guarantee a GUID
does -- two different processes on two different machines could in theory have the same pid and create files
during the same timestamp with the same counter, but the odds of that happening * are so small that it's not
worth giving up the easy time-based sortability based on filenames alone.
Keeping all imposter information under a directory (instead of having metadata outside the directory)
allows us to remove the imposter by simply removing the directory.
There are some extra checks on filesystem operations (atomicWriteFile) due to antivirus software, solar flares,
gremlins, etc. graceful-fs solves some of these, but apparently not all.
Methods
(async) add(stub) → {Object}
Adds a new stub to imposter
Parameters:
Name | Type | Description |
---|---|---|
stub |
Object | the stub to add |
Returns:
- the promise
- Type
- Object
(async) add(imposter) → {Object}
Adds a new imposter
Parameters:
Name | Type | Description |
---|---|---|
imposter |
Object | the imposter to add |
Returns:
- the promise
- Type
- Object
addReference(imposter)
Saves a reference to the imposter so that the functions
(which can't be persisted) can be rehydrated to a loaded imposter.
This means that any data in the function closures will be held in
memory.
Parameters:
Name | Type | Description |
---|---|---|
imposter |
Object | the imposter |
(async) addRequest(request) → {Object}
Adds a request for the imposter
Parameters:
Name | Type | Description |
---|---|---|
request |
Object | the request |
Returns:
- the promise
- Type
- Object
(async) all() → {Object}
Gets all imposters
Returns:
- all imposters keyed by port
- Type
- Object
(async) count() → {Object}
Returns the number of stubs for the imposter
Returns:
- the promise
- Type
- Object
(async) del(id) → {Object}
Deletes the imposter at the given id
Parameters:
Name | Type | Description |
---|---|---|
id |
Number | the id (e.g. the port) |
Returns:
- the deletion promise
- Type
- Object
(async) deleteAll() → {Object}
Deletes all imposters
Returns:
- the deletion promise
- Type
- Object
(async) deleteAtIndex(index) → {Object}
Deletes the stub at the given index
Parameters:
Name | Type | Description |
---|---|---|
index |
Number | the index of the stub to delete |
Returns:
- the promise
- Type
- Object
(async) deleteSavedProxyResponses() → {Object}
Removes the saved proxy responses
Returns:
- Promise
- Type
- Object
(async) deleteSavedRequests() → {Object}
Deletes the requests directory for an imposter
Returns:
- Promise
- Type
- Object
(async) exists(id) → {boolean}
Returns whether an imposter at the given id exists or not
Parameters:
Name | Type | Description |
---|---|---|
id |
Number | the id (e.g. the port) |
Returns:
- Type
- boolean
(async) first(filter, startIndex) → {Object}
Returns the first stub whose predicates matches the filter
Parameters:
Name | Type | Default | Description |
---|---|---|---|
filter |
function | the filter function |
|
startIndex |
Number | 0 | the index to to start searching |
Returns:
- the promise
- Type
- Object
(async) get(id) → {Object}
Gets the imposter by id
Parameters:
Name | Type | Description |
---|---|---|
id |
Number | the id of the imposter (e.g. the port) |
Returns:
- the promise resolving to the imposter
- Type
- Object
(async) insertAtIndex(stub, index) → {Object}
Inserts a new stub at the given index
Parameters:
Name | Type | Description |
---|---|---|
stub |
Object | the stub to add |
index |
Number | the index to insert the new stub at |
Returns:
- the promise
- Type
- Object
(async) loadAll(protocols) → {Object}
Loads all saved imposters at startup
Parameters:
Name | Type | Description |
---|---|---|
protocols |
Object | The protocol map, used to instantiate a new instance |
Returns:
- a promise
- Type
- Object
(async) loadRequests() → {Object}
Returns the saved requests for the imposter
Returns:
- the promise resolving to the array of requests
- Type
- Object
(async) overwriteAll(newStubs) → {Object}
Overwrites all stubs with a new list
Parameters:
Name | Type | Description |
---|---|---|
newStubs |
Object | the new list of stubs |
Returns:
- the promise
- Type
- Object
(async) overwriteAtIndex(stub, index) → {Object}
Overwrites the stub at the given index
Parameters:
Name | Type | Description |
---|---|---|
stub |
Object | the new stub |
index |
Number | the index of the stub to overwrite |
Returns:
- the promise
- Type
- Object
(async) stopAll()
Deletes all imposters; used during testing
stopAllSync()
Deletes all imposters synchronously; used during shutdown
stubsFor(id) → {Object}
Returns the stubs repository for the imposter
Parameters:
Name | Type | Description |
---|---|---|
id |
Number | the id of the imposter |
Returns:
- the stubs repository
- Type
- Object
(async) toJSON(options) → {Object}
Returns a JSON-convertible representation
Parameters:
Name | Type | Description | ||||||
---|---|---|---|---|---|---|---|---|
options |
Object | The formatting options Properties
|
Returns:
- the promise resolving to the JSON object
- Type
- Object
(inner) create(config, logger) → {Object}
Creates the repository
Parameters:
Name | Type | Description | ||||||
---|---|---|---|---|---|---|---|---|
config |
Object | The database configuration Properties
|
||||||
logger |
Object | The logger |
Returns:
- Type
- Object