@oada/list-lib
Advanced tools
Comparing version 5.0.4 to 6.0.0
@@ -57,7 +57,7 @@ /** | ||
* The unique name of this service/watch | ||
*/ | ||
*/ | ||
readonly name: string; | ||
/** | ||
* The unique name of this service/watch | ||
*/ | ||
*/ | ||
readonly timeout: number | undefined; | ||
@@ -64,0 +64,0 @@ constructor(options: Options<Item>); |
@@ -17,4 +17,2 @@ /** | ||
*/ | ||
var _ListWatch_instances, _ListWatch_conn, _ListWatch_watch, _ListWatch_meta, _ListWatch_emitter, _ListWatch_assertItem, _ListWatch_getItem, _ListWatch_emit, _ListWatch_once, _ListWatch_wrapListener, _ListWatch_generate, _ListWatch_initialize, _ListWatch_handleStartingItems, _ListWatch_handleItemChanges, _ListWatch_handleChangeFeed; | ||
import { __classPrivateFieldGet, __classPrivateFieldSet } from "tslib"; | ||
import { on } from 'node:events'; | ||
@@ -24,3 +22,3 @@ import { EventEmitter } from 'eventemitter3'; | ||
import debug from 'debug'; | ||
import { assertNever, buildChangeObject, changeSym, join, } from './util.js'; | ||
import { assertNever, buildChangeObject, changeSym, errorCode, join, } from './util.js'; | ||
import { ChangeType, } from './index.js'; | ||
@@ -50,2 +48,39 @@ import { Metadata } from './Metadata.js'; | ||
export class ListWatch { | ||
/** | ||
* Make ListWatch consider every unknown `Item` new | ||
* @deprecated | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
static AssumeNew = AssumeState.New; | ||
/** | ||
* Make ListWatch consider every unknown `Item` handled | ||
* @deprecated | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
static AssumeHandled = AssumeState.Handled; | ||
/** | ||
* The OADA path of the List being watched | ||
*/ | ||
path; | ||
/** | ||
* The OADA tree of the List being watched | ||
*/ | ||
tree; | ||
/** | ||
* The JSON Path for the list items | ||
*/ | ||
itemsPath; | ||
/** | ||
* The unique name of this service/watch | ||
*/ | ||
name; | ||
/** | ||
* The unique name of this service/watch | ||
*/ | ||
timeout; | ||
#conn; | ||
#watch; | ||
#meta; | ||
#emitter; | ||
#assertItem; | ||
constructor({ path, itemsPath = '$[?(!@property.match(/^_/))]', tree = { '*': { _type: 'application/json' } }, name = process.env.npm_package_name, resume = true, conn, persistInterval = 1000, timeout, | ||
@@ -55,8 +90,2 @@ // If no assert given, assume all items valid | ||
assertItem = () => { }, onAddItem, onChangeItem, onItem, onRemoveItem, onNewList, }) { | ||
_ListWatch_instances.add(this); | ||
_ListWatch_conn.set(this, void 0); | ||
_ListWatch_watch.set(this, void 0); | ||
_ListWatch_meta.set(this, void 0); | ||
_ListWatch_emitter.set(this, void 0); | ||
_ListWatch_assertItem.set(this, void 0); | ||
this.path = path; | ||
@@ -67,25 +96,25 @@ this.tree = tree; | ||
this.timeout = timeout; | ||
__classPrivateFieldSet(this, _ListWatch_conn, conn, "f"); | ||
__classPrivateFieldSet(this, _ListWatch_assertItem, assertItem, "f"); | ||
__classPrivateFieldSet(this, _ListWatch_emitter, new EventEmitter(), "f"); | ||
this.#conn = conn; | ||
this.#assertItem = assertItem; | ||
this.#emitter = new EventEmitter(); | ||
if (onAddItem) { | ||
log.warn('onAddItem is deprecated, use .on(ChangeType.ItemAdded, ...)'); | ||
__classPrivateFieldGet(this, _ListWatch_emitter, "f").on(ChangeType.ItemAdded, __classPrivateFieldGet(this, _ListWatch_instances, "m", _ListWatch_wrapListener).call(this, ChangeType.ItemAdded, async ({ item, pointer }) => onAddItem(await item, pointer))); | ||
this.#emitter.on(ChangeType.ItemAdded, this.#wrapListener(ChangeType.ItemAdded, async ({ item, pointer }) => onAddItem(await item, pointer))); | ||
} | ||
if (onChangeItem) { | ||
log.warn('onChangeItem is deprecated, use .on(ChangeType.ItemChanged, ...)'); | ||
__classPrivateFieldGet(this, _ListWatch_emitter, "f").on(ChangeType.ItemChanged, __classPrivateFieldGet(this, _ListWatch_instances, "m", _ListWatch_wrapListener).call(this, ChangeType.ItemChanged, async ({ change, pointer }) => onChangeItem(change, pointer))); | ||
this.#emitter.on(ChangeType.ItemChanged, this.#wrapListener(ChangeType.ItemChanged, async ({ change, pointer }) => onChangeItem(change, pointer))); | ||
} | ||
if (onItem) { | ||
log.warn('onItem is deprecated, use .on(ChangeType.ItemAny, ...)'); | ||
__classPrivateFieldGet(this, _ListWatch_emitter, "f").on(ChangeType.ItemAny, __classPrivateFieldGet(this, _ListWatch_instances, "m", _ListWatch_wrapListener).call(this, ChangeType.ItemAny, async ({ item, pointer }) => onItem(await item, pointer))); | ||
this.#emitter.on(ChangeType.ItemAny, this.#wrapListener(ChangeType.ItemAny, async ({ item, pointer }) => onItem(await item, pointer))); | ||
} | ||
if (onRemoveItem) { | ||
log.warn('onRemoveItem is deprecated, use .on(ChangeType.ItemRemoved, ...)'); | ||
__classPrivateFieldGet(this, _ListWatch_emitter, "f").on(ChangeType.ItemRemoved, __classPrivateFieldGet(this, _ListWatch_instances, "m", _ListWatch_wrapListener).call(this, ChangeType.ItemRemoved, async ({ pointer }) => onRemoveItem(pointer))); | ||
this.#emitter.on(ChangeType.ItemRemoved, this.#wrapListener(ChangeType.ItemRemoved, async ({ pointer }) => onRemoveItem(pointer))); | ||
} | ||
// Don't persist metdata if service does not "resume" | ||
__classPrivateFieldSet(this, _ListWatch_meta, resume | ||
this.#meta = resume | ||
? new Metadata({ | ||
conn: __classPrivateFieldGet(this, _ListWatch_conn, "f"), | ||
conn: this.#conn, | ||
path, | ||
@@ -95,4 +124,4 @@ name, | ||
}) | ||
: undefined, "f"); | ||
__classPrivateFieldSet(this, _ListWatch_watch, __classPrivateFieldGet(this, _ListWatch_instances, "m", _ListWatch_initialize).call(this, onNewList, timeout), "f"); | ||
: undefined; | ||
this.#watch = this.#initialize(onNewList, timeout); | ||
} | ||
@@ -104,7 +133,7 @@ /** | ||
try { | ||
const watch = await __classPrivateFieldGet(this, _ListWatch_watch, "f"); | ||
const watch = await this.#watch; | ||
await watch.return?.(); | ||
} | ||
finally { | ||
await __classPrivateFieldGet(this, _ListWatch_meta, "f")?.stop(); | ||
await this.#meta?.stop(); | ||
} | ||
@@ -114,6 +143,6 @@ } | ||
if (listener) { | ||
__classPrivateFieldGet(this, _ListWatch_emitter, "f").on(event, __classPrivateFieldGet(this, _ListWatch_instances, "m", _ListWatch_wrapListener).call(this, event, listener)); | ||
this.#emitter.on(event, this.#wrapListener(event, listener)); | ||
return this; | ||
} | ||
return __classPrivateFieldGet(this, _ListWatch_instances, "m", _ListWatch_generate).call(this, event); | ||
return this.#generate(event); | ||
} | ||
@@ -123,273 +152,267 @@ // eslint-disable-next-line @typescript-eslint/promise-function-async | ||
if (listener) { | ||
__classPrivateFieldGet(this, _ListWatch_emitter, "f").once(event, __classPrivateFieldGet(this, _ListWatch_instances, "m", _ListWatch_wrapListener).call(this, event, listener)); | ||
this.#emitter.once(event, this.#wrapListener(event, listener)); | ||
return this; | ||
} | ||
return __classPrivateFieldGet(this, _ListWatch_instances, "m", _ListWatch_once).call(this, event); | ||
return this.#once(event); | ||
} | ||
} | ||
_ListWatch_conn = new WeakMap(), _ListWatch_watch = new WeakMap(), _ListWatch_meta = new WeakMap(), _ListWatch_emitter = new WeakMap(), _ListWatch_assertItem = new WeakMap(), _ListWatch_instances = new WeakSet(), _ListWatch_getItem = | ||
/** | ||
* Fetch the contents of the corresponding list item | ||
*/ | ||
async function _ListWatch_getItem(itemEvent, timeout) { | ||
// Needed because TS is weird about asserts... | ||
const assertItem = __classPrivateFieldGet(this, _ListWatch_assertItem, "f"); | ||
const { data: item } = await __classPrivateFieldGet(this, _ListWatch_conn, "f").get({ | ||
path: join(this.path, itemEvent.pointer), | ||
timeout | ||
}); | ||
assertItem(item); | ||
return item; | ||
}, _ListWatch_emit = | ||
/** | ||
* Emit our internal events | ||
*/ | ||
async function _ListWatch_emit(event, itemEvent) { | ||
// Automagically get the list item when it is accessed | ||
const getItem = __classPrivateFieldGet(this, _ListWatch_instances, "m", _ListWatch_getItem).bind(this); | ||
let itemP; | ||
const out = { | ||
get item() { | ||
if (itemP === undefined) { | ||
itemP = getItem(this); | ||
/** | ||
* Fetch the contents of the corresponding list item | ||
*/ | ||
async #getItem(itemEvent, timeout) { | ||
// Needed because TS is weird about asserts... | ||
const assertItem = this.#assertItem; | ||
const { data: item } = await this.#conn.get({ | ||
path: join(this.path, itemEvent.pointer), | ||
timeout, | ||
}); | ||
assertItem(item); | ||
return item; | ||
} | ||
/** | ||
* Emit our internal events | ||
*/ | ||
async #emit(event, itemEvent) { | ||
// Automagically get the list item when it is accessed | ||
const getItem = this.#getItem.bind(this); | ||
let itemP; | ||
const out = { | ||
get item() { | ||
if (itemP === undefined) { | ||
itemP = getItem(this); | ||
} | ||
return itemP; | ||
}, | ||
...itemEvent, | ||
}; | ||
switch (event) { | ||
case ChangeType.ItemChanged: { | ||
log.debug({ itemChange: itemEvent }, 'Detected change to item'); | ||
this.#emitter.emit(ChangeType.ItemChanged, out); | ||
this.#emitter.emit(ChangeType.ItemAny, out); | ||
break; | ||
} | ||
return itemP; | ||
}, | ||
...itemEvent, | ||
}; | ||
switch (event) { | ||
case ChangeType.ItemChanged: { | ||
log.debug({ itemChange: itemEvent }, 'Detected change to item'); | ||
__classPrivateFieldGet(this, _ListWatch_emitter, "f").emit(ChangeType.ItemChanged, out); | ||
__classPrivateFieldGet(this, _ListWatch_emitter, "f").emit(ChangeType.ItemAny, out); | ||
break; | ||
case ChangeType.ItemAdded: { | ||
log.debug({ itemChange: itemEvent }, 'Detected new item'); | ||
this.#emitter.emit(ChangeType.ItemAdded, out); | ||
this.#emitter.emit(ChangeType.ItemAny, out); | ||
break; | ||
} | ||
case ChangeType.ItemRemoved: { | ||
log.debug({ itemChange: itemEvent }, 'Detected removed item'); | ||
this.#emitter.emit(ChangeType.ItemRemoved, out); | ||
break; | ||
} | ||
case ChangeType.ItemAny: { | ||
throw new TypeError('ItemAny is not a valid event'); | ||
} | ||
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check | ||
default: { | ||
assertNever(event, `Unknown event type ${event}`); | ||
} | ||
} | ||
case ChangeType.ItemAdded: { | ||
log.debug({ itemChange: itemEvent }, 'Detected new item'); | ||
__classPrivateFieldGet(this, _ListWatch_emitter, "f").emit(ChangeType.ItemAdded, out); | ||
__classPrivateFieldGet(this, _ListWatch_emitter, "f").emit(ChangeType.ItemAny, out); | ||
break; | ||
} | ||
async #once(event) { | ||
const generator = this.#generate(event); | ||
try { | ||
const { value } = await generator.next(); | ||
return value; | ||
} | ||
case ChangeType.ItemRemoved: { | ||
log.debug({ itemChange: itemEvent }, 'Detected removed item'); | ||
__classPrivateFieldGet(this, _ListWatch_emitter, "f").emit(ChangeType.ItemRemoved, out); | ||
break; | ||
finally { | ||
await generator.return(); | ||
} | ||
case ChangeType.ItemAny: { | ||
throw new TypeError('ItemAny is not a valid event'); | ||
} | ||
default: { | ||
assertNever(event, `Unknown event type ${event}`); | ||
} | ||
} | ||
}, _ListWatch_once = async function _ListWatch_once(event) { | ||
const generator = __classPrivateFieldGet(this, _ListWatch_instances, "m", _ListWatch_generate).call(this, event); | ||
try { | ||
const { value } = await generator.next(); | ||
return value; | ||
#wrapListener(type, listener) { | ||
return async (itemChange) => { | ||
try { | ||
await listener(itemChange); | ||
} | ||
catch (error) { | ||
log.error({ type, listener: listener.name, error }, 'Error in listener'); | ||
await this.#meta?.setErrored(itemChange.pointer, itemChange.listRev, error); | ||
} | ||
finally { | ||
if (this.#meta) { | ||
// Update our place in the change feed? | ||
this.#meta.rev = itemChange.listRev; | ||
} | ||
} | ||
}; | ||
} | ||
finally { | ||
await generator.return(); | ||
async *#generate(type) { | ||
const events = on(this.#emitter, type); | ||
for await (const [event] of events) { | ||
try { | ||
// Generate event | ||
yield [event]; | ||
} | ||
catch (error) { | ||
log.error({ type, error }, 'Error in generator'); | ||
await this.#meta?.setErrored(event.pointer, event.listRev, error); | ||
} | ||
finally { | ||
if (this.#meta) { | ||
// Update our place in the change feed? | ||
this.#meta.rev = event.listRev; | ||
} | ||
} | ||
} | ||
} | ||
}, _ListWatch_wrapListener = function _ListWatch_wrapListener(type, listener) { | ||
return async (itemChange) => { | ||
/** | ||
* Do async stuff for initializing ourself since constructors are synchronous | ||
*/ | ||
async #initialize(assume = AssumeState.New, timeout) { | ||
const { path } = this; | ||
const conn = this.#conn; | ||
log.debug('Ensuring %s exists', path); | ||
try { | ||
await listener(itemChange); | ||
await conn.head({ path, timeout }); | ||
} | ||
catch (error) { | ||
log.error({ type, listener: listener.name, error }, 'Error in listener'); | ||
await __classPrivateFieldGet(this, _ListWatch_meta, "f")?.setErrored(itemChange.pointer, itemChange.listRev, error); | ||
} | ||
finally { | ||
if (__classPrivateFieldGet(this, _ListWatch_meta, "f")) { | ||
// Update our place in the change feed? | ||
__classPrivateFieldGet(this, _ListWatch_meta, "f").rev = itemChange.listRev; | ||
if (['403', '404'].includes(errorCode(error) ?? '')) { | ||
// Create it | ||
await conn.put({ path, data: {}, timeout }); | ||
log.trace('Created %s because it did not exist', path); | ||
} | ||
else { | ||
log.error({ error }); | ||
throw error; | ||
} | ||
} | ||
}; | ||
}, _ListWatch_generate = async function* _ListWatch_generate(type) { | ||
const events = on(__classPrivateFieldGet(this, _ListWatch_emitter, "f"), type); | ||
for await (const [event] of events) { | ||
try { | ||
// Generate event | ||
yield [event]; | ||
} | ||
catch (error) { | ||
log.error({ type, error }, 'Error in generator'); | ||
await __classPrivateFieldGet(this, _ListWatch_meta, "f")?.setErrored(event.pointer, event.listRev, error); | ||
} | ||
finally { | ||
if (__classPrivateFieldGet(this, _ListWatch_meta, "f")) { | ||
// Update our place in the change feed? | ||
__classPrivateFieldGet(this, _ListWatch_meta, "f").rev = event.listRev; | ||
const foundMeta = await this.#meta?.init(); | ||
log.debug('Resuming watch from rev %s', this.#meta?.rev); | ||
// Setup watch on the path | ||
const { changes } = await conn.watch({ | ||
path, | ||
rev: this.#meta?.rev, | ||
type: 'tree', | ||
timeout, | ||
}); | ||
if (!foundMeta) { | ||
switch (assume) { | ||
case AssumeState.Handled: { | ||
break; | ||
} | ||
case AssumeState.New: { | ||
await this.#handleStartingItems(timeout); | ||
break; | ||
} | ||
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check | ||
default: { | ||
assertNever(assume); | ||
} | ||
} | ||
} | ||
// eslint-disable-next-line github/no-then | ||
void this.#handleChangeFeed(changes).catch((error) => | ||
// Forward rejections to EventEmitter | ||
this.#emitter.emit('error', error)); | ||
log.info({ this: this }, 'ListWatch initialized'); | ||
return changes; | ||
} | ||
}, _ListWatch_initialize = | ||
/** | ||
* Do async stuff for initializing ourself since constructors are synchronous | ||
*/ | ||
async function _ListWatch_initialize(assume = AssumeState.New, timeout) { | ||
const { path } = this; | ||
const conn = __classPrivateFieldGet(this, _ListWatch_conn, "f"); | ||
log.debug('Ensuring %s exists', path); | ||
try { | ||
await conn.head({ path, timeout }); | ||
} | ||
catch (error) { | ||
// @ts-expect-error darn errors | ||
if (error?.status === 403 || error?.status === 404 || error?.code === '403' || error?.code === '404') { | ||
// Create it | ||
await conn.put({ path, data: {}, timeout }); | ||
log.trace('Created %s because it did not exist', path); | ||
/** | ||
* Treat all starting list items as new | ||
* | ||
* @todo Remove need for tree GET | ||
*/ | ||
async #handleStartingItems(timeout) { | ||
const { path, tree, itemsPath } = this; | ||
const { data: json } = await this.#conn.get({ path, tree, timeout }); | ||
if (typeof json !== 'object' || | ||
json === null || | ||
Array.isArray(json) || | ||
Buffer.isBuffer(json)) { | ||
throw new TypeError('Expected JSON'); | ||
} | ||
else { | ||
log.error({ error }); | ||
throw error; | ||
// eslint-disable-next-line new-cap | ||
const items = JSONPath({ | ||
resultType: 'all', | ||
path: itemsPath, | ||
json, | ||
}); | ||
const listRev = Number(json._rev); | ||
for await (const { value, pointer } of items) { | ||
const itemChange = { | ||
item: value, | ||
listRev, | ||
pointer, | ||
}; | ||
await this.#emit(ChangeType.ItemAdded, itemChange); | ||
} | ||
} | ||
const foundMeta = await __classPrivateFieldGet(this, _ListWatch_meta, "f")?.init(); | ||
log.debug('Resuming watch from rev %s', __classPrivateFieldGet(this, _ListWatch_meta, "f")?.rev); | ||
// Setup watch on the path | ||
const { changes } = await conn.watch({ | ||
path, | ||
rev: __classPrivateFieldGet(this, _ListWatch_meta, "f")?.rev, | ||
type: 'tree', | ||
timeout, | ||
}); | ||
if (!foundMeta) { | ||
switch (assume) { | ||
case AssumeState.Handled: { | ||
break; | ||
/** | ||
* Iterate though chid changes to list items | ||
*/ | ||
async #handleItemChanges(changeBody, listRev) { | ||
// eslint-disable-next-line new-cap | ||
const items = JSONPath({ | ||
resultType: 'all', | ||
path: this.itemsPath, | ||
json: changeBody, | ||
}); | ||
for await (const { value, pointer } of items) { | ||
if (value === undefined) { | ||
// Item was removed from list | ||
const itemChange = { | ||
listRev, | ||
pointer, | ||
}; | ||
await this.#emit(ChangeType.ItemRemoved, itemChange); | ||
continue; | ||
} | ||
case AssumeState.New: { | ||
await __classPrivateFieldGet(this, _ListWatch_instances, "m", _ListWatch_handleStartingItems).call(this, timeout); | ||
break; | ||
const { [changeSym]: changes } = value; | ||
if (!changes && typeof value === 'object' && '_id' in value) { | ||
// Item was added to list? | ||
const itemChange = { | ||
listRev, | ||
pointer, | ||
}; | ||
await this.#emit(ChangeType.ItemAdded, itemChange); | ||
continue; | ||
} | ||
default: { | ||
assertNever(assume); | ||
for await (const change of changes ?? []) { | ||
log.trace({ change }, 'Received change'); | ||
const rev = Number( | ||
// @ts-expect-error just do it | ||
change.body?._meta?._rev ?? change.body?._rev); | ||
// ???: Find any children of change | ||
// const changes = [change]; | ||
const itemChange = { | ||
rev, | ||
listRev, | ||
pointer, | ||
change: { | ||
...change, | ||
// Adust change path to start at this item | ||
path: change.path.slice(pointer.length), | ||
}, | ||
}; | ||
// Emit generic item change event | ||
await this.#emit(ChangeType.ItemChanged, itemChange); | ||
} | ||
} | ||
} | ||
// eslint-disable-next-line github/no-then | ||
void __classPrivateFieldGet(this, _ListWatch_instances, "m", _ListWatch_handleChangeFeed).call(this, changes).catch((error) => | ||
// Forward rejections to EventEmitter | ||
__classPrivateFieldGet(this, _ListWatch_emitter, "f").emit('error', error)); | ||
log.info({ this: this }, 'ListWatch initialized'); | ||
return changes; | ||
}, _ListWatch_handleStartingItems = | ||
/** | ||
* Treat all starting list items as new | ||
* | ||
* @todo Remove need for tree GET | ||
*/ | ||
async function _ListWatch_handleStartingItems(timeout) { | ||
const { path, tree, itemsPath } = this; | ||
const { data: json } = await __classPrivateFieldGet(this, _ListWatch_conn, "f").get({ path, tree, timeout }); | ||
if (typeof json !== 'object' || | ||
json === null || | ||
Array.isArray(json) || | ||
Buffer.isBuffer(json)) { | ||
throw new TypeError('Expected JSON'); | ||
} | ||
// eslint-disable-next-line new-cap | ||
const items = JSONPath({ | ||
resultType: 'all', | ||
path: itemsPath, | ||
json, | ||
}); | ||
const listRev = Number(json._rev); | ||
for await (const { value, pointer } of items) { | ||
const itemChange = { | ||
item: value, | ||
listRev, | ||
pointer, | ||
}; | ||
await __classPrivateFieldGet(this, _ListWatch_instances, "m", _ListWatch_emit).call(this, ChangeType.ItemAdded, itemChange); | ||
} | ||
}, _ListWatch_handleItemChanges = | ||
/** | ||
* Iterate though chid changes to list items | ||
*/ | ||
async function _ListWatch_handleItemChanges(changeBody, listRev) { | ||
// eslint-disable-next-line new-cap | ||
const items = JSONPath({ | ||
resultType: 'all', | ||
path: this.itemsPath, | ||
json: changeBody, | ||
}); | ||
for await (const { value, pointer } of items) { | ||
if (value === undefined) { | ||
// Item was removed from list | ||
const itemChange = { | ||
listRev, | ||
pointer, | ||
}; | ||
await __classPrivateFieldGet(this, _ListWatch_instances, "m", _ListWatch_emit).call(this, ChangeType.ItemRemoved, itemChange); | ||
continue; | ||
} | ||
const { [changeSym]: changes } = value; | ||
if (!changes && typeof value === 'object' && '_id' in value) { | ||
// Item was added to list? | ||
const itemChange = { | ||
listRev, | ||
pointer, | ||
}; | ||
await __classPrivateFieldGet(this, _ListWatch_instances, "m", _ListWatch_emit).call(this, ChangeType.ItemAdded, itemChange); | ||
continue; | ||
} | ||
for await (const change of changes ?? []) { | ||
log.trace({ change }, 'Received change'); | ||
const rev = Number( | ||
async #handleChangeFeed(watch) { | ||
// Iterate through list change feed | ||
for await (const [rootChange, ...children] of watch) { | ||
const listRev = Number( | ||
// @ts-expect-error just do it | ||
change.body?._meta?._rev ?? change.body?._rev); | ||
// ???: Find any children of change | ||
// const changes = [change]; | ||
const itemChange = { | ||
rev, | ||
listRev, | ||
pointer, | ||
change: { | ||
...change, | ||
// Adust change path to start at this item | ||
path: change.path.slice(pointer.length), | ||
}, | ||
}; | ||
// Emit generic item change event | ||
await __classPrivateFieldGet(this, _ListWatch_instances, "m", _ListWatch_emit).call(this, ChangeType.ItemChanged, itemChange); | ||
rootChange.body?._meta?._rev ?? rootChange.body?._rev); | ||
if (rootChange.body === null && | ||
rootChange.type === 'delete' && | ||
rootChange.path === '') { | ||
// The list itself was deleted | ||
log.warn('Detected delete of list %s, nothing left to watch', rootChange.path); | ||
break; | ||
} | ||
const changeBody = buildChangeObject(rootChange, ...children); | ||
await this.#handleItemChanges(changeBody, listRev); | ||
if (this.#meta) { | ||
log.trace('Received change to root of list, updating handled rev in our _meta records'); | ||
this.#meta.rev = rootChange.body?._rev; | ||
} | ||
} | ||
log.fatal('Change feed ended unexpectedly'); | ||
throw new Error('Change feed ended'); | ||
} | ||
}, _ListWatch_handleChangeFeed = async function _ListWatch_handleChangeFeed(watch) { | ||
// Iterate through list change feed | ||
for await (const [rootChange, ...children] of watch) { | ||
const listRev = Number( | ||
// @ts-expect-error just do it | ||
rootChange.body?._meta?._rev ?? rootChange.body?._rev); | ||
if (rootChange.body === null && | ||
rootChange.type === 'delete' && | ||
rootChange.path === '') { | ||
// The list itself was deleted | ||
log.warn('Detected delete of list %s, nothing left to watch', rootChange.path); | ||
break; | ||
} | ||
const changeBody = buildChangeObject(rootChange, ...children); | ||
await __classPrivateFieldGet(this, _ListWatch_instances, "m", _ListWatch_handleItemChanges).call(this, changeBody, listRev); | ||
if (__classPrivateFieldGet(this, _ListWatch_meta, "f")) { | ||
log.trace('Received change to root of list, updating handled rev in our _meta records'); | ||
__classPrivateFieldGet(this, _ListWatch_meta, "f").rev = rootChange.body?._rev; | ||
} | ||
} | ||
log.fatal('Change feed ended unexpectedly'); | ||
throw new Error('Change feed ended'); | ||
}; | ||
/** | ||
* Make ListWatch consider every unknown `Item` new | ||
* @deprecated | ||
*/ | ||
ListWatch.AssumeNew = AssumeState.New; | ||
/** | ||
* Make ListWatch consider every unknown `Item` handled | ||
* @deprecated | ||
*/ | ||
ListWatch.AssumeHandled = AssumeState.Handled; | ||
} | ||
//# sourceMappingURL=ListWatch.js.map |
@@ -17,4 +17,2 @@ /** | ||
*/ | ||
var _Metadata_instances, _Metadata_rev, _Metadata_revDirty, _Metadata_conn, _Metadata_path, _Metadata_initialized, _Metadata_controller, _Metadata_updates, _Metadata_doUpdate; | ||
import { __classPrivateFieldGet, __classPrivateFieldSet } from "tslib"; | ||
import { inspect } from 'node:util'; | ||
@@ -25,3 +23,3 @@ import { AbortController } from 'abort-controller'; | ||
import { assert as assertResource } from '@oada/types/oada/resource.js'; | ||
import { join } from './util.js'; | ||
import { errorCode, join } from './util.js'; | ||
const log = { | ||
@@ -47,22 +45,21 @@ trace: debug('@oada/list-lib#metadata:trace'), | ||
} | ||
/** | ||
* The rev we left off on | ||
*/ | ||
#rev; | ||
#revDirty = false; | ||
// Where to store state | ||
#conn; | ||
#path; | ||
#initialized = false; | ||
#controller; | ||
#updates; | ||
constructor({ conn, path, name, persistInterval, }) { | ||
_Metadata_instances.add(this); | ||
/** | ||
* The rev we left off on | ||
*/ | ||
_Metadata_rev.set(this, void 0); | ||
_Metadata_revDirty.set(this, false); | ||
// Where to store state | ||
_Metadata_conn.set(this, void 0); | ||
_Metadata_path.set(this, void 0); | ||
_Metadata_initialized.set(this, false); | ||
_Metadata_controller.set(this, void 0); | ||
_Metadata_updates.set(this, void 0); | ||
__classPrivateFieldSet(this, _Metadata_conn, conn, "f"); | ||
__classPrivateFieldSet(this, _Metadata_path, join(path, '_meta', Metadata.META_KEY, name), "f"); | ||
__classPrivateFieldSet(this, _Metadata_controller, new AbortController(), "f"); | ||
this.#conn = conn; | ||
this.#path = join(path, '_meta', Metadata.META_KEY, name); | ||
this.#controller = new AbortController(); | ||
// ??? Use timeouts for all updates? | ||
const revUpdateInterval = setInterval(persistInterval, undefined, { | ||
// @ts-expect-error browser/node difference bs | ||
signal: __classPrivateFieldGet(this, _Metadata_controller, "f").signal, | ||
signal: this.#controller.signal, | ||
}); | ||
@@ -72,19 +69,19 @@ const updateRevs = async () => { | ||
for await (const _ of revUpdateInterval) { | ||
await __classPrivateFieldGet(this, _Metadata_instances, "m", _Metadata_doUpdate).call(this); | ||
await this.#doUpdate(); | ||
} | ||
} | ||
finally { | ||
await __classPrivateFieldGet(this, _Metadata_instances, "m", _Metadata_doUpdate).call(this); | ||
await this.#doUpdate(); | ||
} | ||
}; | ||
__classPrivateFieldSet(this, _Metadata_updates, updateRevs(), "f"); | ||
this.#updates = updateRevs(); | ||
} | ||
async stop() { | ||
__classPrivateFieldGet(this, _Metadata_controller, "f").abort(); | ||
await __classPrivateFieldGet(this, _Metadata_updates, "f"); | ||
this.#controller.abort(); | ||
await this.#updates; | ||
} | ||
async setErrored(pointer, rev, error) { | ||
// Merge with current info | ||
await __classPrivateFieldGet(this, _Metadata_conn, "f")?.put({ | ||
path: __classPrivateFieldGet(this, _Metadata_path, "f"), | ||
await this.#conn?.put({ | ||
path: this.#path, | ||
data: { | ||
@@ -107,40 +104,67 @@ errors: { | ||
try { | ||
const { data } = await __classPrivateFieldGet(this, _Metadata_conn, "f").get({ | ||
path: __classPrivateFieldGet(this, _Metadata_path, "f"), | ||
const { data } = await this.#conn.get({ | ||
path: this.#path, | ||
}); | ||
assertResource(data); | ||
__classPrivateFieldSet(this, _Metadata_rev, Number(data.rev ?? 0), "f"); | ||
this.#rev = Number(data.rev ?? 0); | ||
return true; | ||
} | ||
catch { | ||
// Create our metadata? | ||
log.info('%s does not exist, posting new resource', __classPrivateFieldGet(this, _Metadata_path, "f")); | ||
const { headers: { 'content-location': location }, } = await __classPrivateFieldGet(this, _Metadata_conn, "f").post({ | ||
path: '/resources/', | ||
data: {}, | ||
contentType: 'application/json', | ||
}); | ||
const { headers: { 'x-oada-rev': revHeader }, } = await __classPrivateFieldGet(this, _Metadata_conn, "f").put({ | ||
path: __classPrivateFieldGet(this, _Metadata_path, "f"), | ||
data: { _id: location?.slice(1) }, | ||
}); | ||
const rev = revHeader ? Number(revHeader) : undefined; | ||
__classPrivateFieldSet(this, _Metadata_rev, rev, "f"); | ||
await __classPrivateFieldGet(this, _Metadata_conn, "f").put({ | ||
path: __classPrivateFieldGet(this, _Metadata_path, "f"), | ||
data: { | ||
rev: rev, | ||
}, | ||
}); | ||
return false; | ||
catch (error) { | ||
if (errorCode(error) !== '404') { | ||
// Pass other errors causes up | ||
throw new Error('List init error', { cause: error }); | ||
} | ||
return await this.#createMeta(); | ||
} | ||
finally { | ||
__classPrivateFieldSet(this, _Metadata_initialized, true, "f"); | ||
this.#initialized = true; | ||
} | ||
} | ||
/** | ||
* Create our metadata | ||
*/ | ||
async #createMeta() { | ||
log.info('%s does not exist, posting new resource', this.#path); | ||
const { headers: { 'content-location': location }, } = await this.#conn.post({ | ||
path: '/resources/', | ||
data: {}, | ||
contentType: 'application/json', | ||
}); | ||
const { headers: { 'x-oada-rev': revHeader }, } = await this.#conn.put({ | ||
path: this.#path, | ||
data: { _id: location?.slice(1) }, | ||
}); | ||
const rev = revHeader ? Number(revHeader) : undefined; | ||
this.#rev = rev; | ||
await this.#conn.put({ | ||
path: this.#path, | ||
data: { | ||
rev: rev, | ||
}, | ||
}); | ||
return false; | ||
} | ||
async #doUpdate() { | ||
if (!(this.#initialized && this.#revDirty)) { | ||
return; | ||
} | ||
log.trace('Recording rev %s', this.#rev); | ||
const data = { rev: this.#rev }; | ||
this.#revDirty = false; | ||
try { | ||
await this.#conn.put({ | ||
path: this.#path, | ||
data, | ||
}); | ||
} | ||
catch (error) { | ||
log.error({ error }, 'Failed to update rev'); | ||
this.#revDirty = true; | ||
} | ||
} | ||
get rev() { | ||
return __classPrivateFieldGet(this, _Metadata_rev, "f"); | ||
return this.#rev; | ||
} | ||
set rev(rev) { | ||
if (__classPrivateFieldGet(this, _Metadata_rev, "f") === rev) { | ||
if (this.#rev === rev) { | ||
// No need to update | ||
@@ -150,24 +174,6 @@ return; | ||
log.trace('Updating local rev to %d', rev); | ||
__classPrivateFieldSet(this, _Metadata_rev, rev, "f"); | ||
__classPrivateFieldSet(this, _Metadata_revDirty, true, "f"); | ||
this.#rev = rev; | ||
this.#revDirty = true; | ||
} | ||
} | ||
_Metadata_rev = new WeakMap(), _Metadata_revDirty = new WeakMap(), _Metadata_conn = new WeakMap(), _Metadata_path = new WeakMap(), _Metadata_initialized = new WeakMap(), _Metadata_controller = new WeakMap(), _Metadata_updates = new WeakMap(), _Metadata_instances = new WeakSet(), _Metadata_doUpdate = async function _Metadata_doUpdate() { | ||
if (!(__classPrivateFieldGet(this, _Metadata_initialized, "f") && __classPrivateFieldGet(this, _Metadata_revDirty, "f"))) { | ||
return; | ||
} | ||
log.trace('Recording rev %s', __classPrivateFieldGet(this, _Metadata_rev, "f")); | ||
const data = { rev: __classPrivateFieldGet(this, _Metadata_rev, "f") }; | ||
__classPrivateFieldSet(this, _Metadata_revDirty, false, "f"); | ||
try { | ||
await __classPrivateFieldGet(this, _Metadata_conn, "f").put({ | ||
path: __classPrivateFieldGet(this, _Metadata_path, "f"), | ||
data, | ||
}); | ||
} | ||
catch (error) { | ||
log.error({ error }, 'Failed to update rev'); | ||
__classPrivateFieldSet(this, _Metadata_revDirty, true, "f"); | ||
} | ||
}; | ||
//# sourceMappingURL=Metadata.js.map |
@@ -17,2 +17,6 @@ /** | ||
*/ | ||
export {}; | ||
export declare function errorCode(error: { | ||
code?: string; | ||
status?: number; | ||
statusCode?: number; | ||
}): string | undefined; |
@@ -73,2 +73,6 @@ /** | ||
} | ||
export function errorCode(error) { | ||
const code = error.code ?? error.status ?? error.statusCode; | ||
return code?.toString(); | ||
} | ||
/** | ||
@@ -75,0 +79,0 @@ * @internal |
{ | ||
"name": "@oada/list-lib", | ||
"version": "5.0.4", | ||
"version": "6.0.0", | ||
"description": "Library for processing items in an OADA list", | ||
@@ -8,3 +8,3 @@ "main": "dist/index.js", | ||
"engines": { | ||
"node": ">=16.0.0" | ||
"node": ">=18.0.0" | ||
}, | ||
@@ -36,6 +36,2 @@ "repository": { | ||
], | ||
"prettier": { | ||
"singleQuote": true, | ||
"quoteProps": "consistent" | ||
}, | ||
"ava": { | ||
@@ -70,26 +66,26 @@ "failFast": false, | ||
"devDependencies": { | ||
"@ava/typescript": "^4.1.0", | ||
"@oada/client": "^5.0.1", | ||
"@tsconfig/node16": "^16.1.1", | ||
"@ava/typescript": "^5.0.0", | ||
"@oada/client": "^5.1.0", | ||
"@tsconfig/node18": "^18.2.4", | ||
"@types/debug": "^4.1.12", | ||
"@types/node": "^16.18.80", | ||
"@types/node": "^18.19.39", | ||
"@types/object-assign-deep": "^0.4.3", | ||
"@types/sinon": "^17.0.3", | ||
"@typescript-eslint/eslint-plugin": "^6.21.0", | ||
"@typescript-eslint/parser": "^6.21.0", | ||
"@yarnpkg/sdks": "^3.1.0", | ||
"ava": "6.1.1", | ||
"c8": "^9.1.0", | ||
"eslint": "^8.56.0", | ||
"@typescript-eslint/eslint-plugin": "^7.14.1", | ||
"@typescript-eslint/parser": "^7.14.1", | ||
"@yarnpkg/sdks": "^3.1.3", | ||
"ava": "6.1.3", | ||
"c8": "^10.1.2", | ||
"eslint": "^8.57.0", | ||
"eslint-config-prettier": "^9.1.0", | ||
"eslint-config-xo": "^0.44.0", | ||
"eslint-config-xo-typescript": "^2.0.0", | ||
"eslint-config-xo": "^0.45.0", | ||
"eslint-config-xo-typescript": "^4.0.0", | ||
"eslint-formatter-pretty": "^6.0.1", | ||
"eslint-import-resolver-node": "^0.3.9", | ||
"eslint-plugin-array-func": "^5.0.1", | ||
"eslint-plugin-ava": "^14.0.0", | ||
"eslint-plugin-ava": "^15.0.1", | ||
"eslint-plugin-escompat": "^3.4.0", | ||
"eslint-plugin-eslint-comments": "^3.2.0", | ||
"eslint-plugin-filenames": "^1.3.2", | ||
"eslint-plugin-github": "^4.10.1", | ||
"eslint-plugin-github": "^5.0.1", | ||
"eslint-plugin-i18n-text": "^1.0.1", | ||
@@ -99,16 +95,16 @@ "eslint-plugin-import": "^2.29.1", | ||
"eslint-plugin-no-only-tests": "^3.1.0", | ||
"eslint-plugin-no-secrets": "^0.8.9", | ||
"eslint-plugin-no-secrets": "^1.0.2", | ||
"eslint-plugin-node": "^11.1.0", | ||
"eslint-plugin-notice": "^0.9.10", | ||
"eslint-plugin-notice": "^1.0.0", | ||
"eslint-plugin-optimize-regex": "^1.2.1", | ||
"eslint-plugin-prettier": "^5.1.3", | ||
"eslint-plugin-promise": "^6.1.1", | ||
"eslint-plugin-regexp": "^2.2.0", | ||
"eslint-plugin-security": "^2.1.0", | ||
"eslint-plugin-sonarjs": "^0.24.0", | ||
"eslint-plugin-unicorn": "^51.0.1", | ||
"prettier": "^3.2.5", | ||
"sinon": "^17.0.1", | ||
"eslint-plugin-promise": "^6.2.0", | ||
"eslint-plugin-regexp": "^2.6.0", | ||
"eslint-plugin-security": "^3.0.1", | ||
"eslint-plugin-sonarjs": "^1.0.3", | ||
"eslint-plugin-unicorn": "^54.0.0", | ||
"prettier": "^3.3.2", | ||
"sinon": "^18.0.0", | ||
"ts-node": "^10.9.2", | ||
"typescript": "5.3.3" | ||
"typescript": "5.5.2" | ||
}, | ||
@@ -118,10 +114,10 @@ "dependencies": { | ||
"abort-controller": "^3.0.0", | ||
"debug": "^4.3.4", | ||
"debug": "^4.3.5", | ||
"eventemitter3": "^5.0.1", | ||
"isomorphic-timers-promises": "^1.0.1", | ||
"json-ptr": "^3.1.1", | ||
"jsonpath-plus": "^8.0.0", | ||
"jsonpath-plus": "^9.0.0", | ||
"object-assign-deep": "^0.4.0", | ||
"tslib": "^2.6.2", | ||
"xksuid": "https://github.com/g12i/xksuid.git#fix-crypto-polyfill" | ||
"tslib": "^2.6.3", | ||
"xksuid": "https://github.com/g12i/xksuid.git#commit=22a21eb01331191feb62ea0a4e67d96d2e80f396" | ||
}, | ||
@@ -136,5 +132,5 @@ "peerDependencies": { | ||
}, | ||
"packageManager": "yarn@4.1.1", | ||
"packageManager": "yarn@4.3.1", | ||
"volta": { | ||
"node": "20.11.0" | ||
"node": "20.15.0" | ||
}, | ||
@@ -141,0 +137,0 @@ "resolutions": { |
@@ -19,3 +19,3 @@ # @oada/list-lib | ||
```typescript | ||
import { ChangeType, ListWatch } from '@oada/list-lib' | ||
import { ChangeType, ListWatch, AssumeState } from '@oada/list-lib' | ||
@@ -25,3 +25,10 @@ // See type definitions for all supported options | ||
path: '/bookmarks/foo/list', | ||
assertItem: /* assertion function to run on each item handled */, | ||
conn: /* an @oada/client instance */, | ||
name: /* string; key name of the oada-list-lib entry in the _meta doc*/, | ||
resume: /* boolean; whether to track changes using a _meta/oada-list-lib/<name> entry */, | ||
onNewList: /* AssumeState.New || AssumeState.Handled; Whether or not to handle existing | ||
list items on startup. `New` means it will treat the list as new every time | ||
it starts up and will attempt to process each item; `Handled` means it will | ||
not process existing items. */, | ||
}) | ||
@@ -28,0 +35,0 @@ |
@@ -34,2 +34,3 @@ /** | ||
changeSym, | ||
errorCode, | ||
join, | ||
@@ -74,2 +75,3 @@ } from './util.js'; | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
static readonly AssumeNew = AssumeState.New; | ||
@@ -80,2 +82,3 @@ /** | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
static readonly AssumeHandled = AssumeState.Handled; | ||
@@ -97,7 +100,7 @@ | ||
* The unique name of this service/watch | ||
*/ | ||
*/ | ||
readonly name; | ||
/** | ||
* The unique name of this service/watch | ||
*/ | ||
*/ | ||
readonly timeout; | ||
@@ -258,3 +261,3 @@ | ||
path: join(this.path, itemEvent.pointer), | ||
timeout | ||
timeout, | ||
}); | ||
@@ -315,2 +318,3 @@ assertItem(item); | ||
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check | ||
default: { | ||
@@ -388,6 +392,5 @@ assertNever(event, `Unknown event type ${event}`); | ||
try { | ||
await conn.head({ path, timeout}); | ||
await conn.head({ path, timeout }); | ||
} catch (error: unknown) { | ||
// @ts-expect-error darn errors | ||
if (error?.status === 403 || error?.status === 404 || error?.code === '403' || error?.code === '404') { | ||
if (['403', '404'].includes(errorCode(error as Error) ?? '')) { | ||
// Create it | ||
@@ -424,2 +427,3 @@ await conn.put({ path, data: {}, timeout }); | ||
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check | ||
default: { | ||
@@ -464,3 +468,3 @@ assertNever(assume); | ||
}); | ||
const listRev = Number((json as unknown as {_rev: number})._rev); | ||
const listRev = Number((json as unknown as { _rev: number })._rev); | ||
for await (const { value, pointer } of items) { | ||
@@ -467,0 +471,0 @@ const itemChange = { |
@@ -28,4 +28,4 @@ /** | ||
import { errorCode, join } from './util.js'; | ||
import type { Conn } from './Options.js'; | ||
import { join } from './util.js'; | ||
@@ -163,29 +163,9 @@ const log = { | ||
return true; | ||
} catch { | ||
// Create our metadata? | ||
log.info('%s does not exist, posting new resource', this.#path); | ||
const { | ||
headers: { 'content-location': location }, | ||
} = await this.#conn.post({ | ||
path: '/resources/', | ||
data: {}, | ||
contentType: 'application/json', | ||
}); | ||
const { | ||
headers: { 'x-oada-rev': revHeader }, | ||
} = await this.#conn.put({ | ||
path: this.#path, | ||
data: { _id: location?.slice(1) }, | ||
}); | ||
} catch (error: unknown) { | ||
if (errorCode(error as Error) !== '404') { | ||
// Pass other errors causes up | ||
throw new Error('List init error', { cause: error }); | ||
} | ||
const rev = revHeader ? Number(revHeader) : undefined; | ||
this.#rev = rev; | ||
await this.#conn.put({ | ||
path: this.#path, | ||
data: { | ||
rev: rev!, | ||
}, | ||
}); | ||
return false; | ||
return await this.#createMeta(); | ||
} finally { | ||
@@ -196,2 +176,34 @@ this.#initialized = true; | ||
/** | ||
* Create our metadata | ||
*/ | ||
async #createMeta() { | ||
log.info('%s does not exist, posting new resource', this.#path); | ||
const { | ||
headers: { 'content-location': location }, | ||
} = await this.#conn.post({ | ||
path: '/resources/', | ||
data: {}, | ||
contentType: 'application/json', | ||
}); | ||
const { | ||
headers: { 'x-oada-rev': revHeader }, | ||
} = await this.#conn.put({ | ||
path: this.#path, | ||
data: { _id: location?.slice(1) }, | ||
}); | ||
const rev = revHeader ? Number(revHeader) : undefined; | ||
this.#rev = rev; | ||
await this.#conn.put({ | ||
path: this.#path, | ||
data: { | ||
rev: rev!, | ||
}, | ||
}); | ||
return false; | ||
} | ||
async #doUpdate() { | ||
@@ -198,0 +210,0 @@ if (!(this.#initialized && this.#revDirty)) { |
@@ -97,6 +97,5 @@ /** | ||
/** | ||
* Timeout for the watches created in OADAClient | ||
* Timeout for the watches created in OADAClient | ||
*/ | ||
timeout?: number; | ||
} | ||
@@ -103,0 +102,0 @@ |
@@ -20,1 +20,5 @@ /** | ||
} | ||
interface Error { | ||
code?: string; | ||
} |
@@ -119,2 +119,11 @@ /** | ||
export function errorCode(error: { | ||
code?: string; | ||
status?: number; | ||
statusCode?: number; | ||
}): string | undefined { | ||
const code = error.code ?? error.status ?? error.statusCode; | ||
return code?.toString(); | ||
} | ||
/** | ||
@@ -121,0 +130,0 @@ * @internal |
{ | ||
"extends": "@tsconfig/node16", | ||
"extends": "@tsconfig/node18", | ||
"ts-node": { | ||
@@ -4,0 +4,0 @@ "files": true |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
HTTP dependency
Supply chain riskContains a dependency which resolves to a remote HTTP URL which could be used to inject untrusted code and reduce overall package reliability.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
HTTP dependency
Supply chain riskContains a dependency which resolves to a remote HTTP URL which could be used to inject untrusted code and reduce overall package reliability.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
61
1
102219
26
2152
+ Addedjsonpath-plus@9.0.0(transitive)
- Removedjsonpath-plus@8.1.0(transitive)
Updateddebug@^4.3.5
Updatedjsonpath-plus@^9.0.0
Updatedtslib@^2.6.3
Updatedxksuid@https://github.com/g12i/xksuid.git#commit=22a21eb01331191feb62ea0a4e67d96d2e80f396