Skip to content

feat: creating readonly option#8

Open
Farenheith wants to merge 1 commit intomasterfrom
readonly
Open

feat: creating readonly option#8
Farenheith wants to merge 1 commit intomasterfrom
readonly

Conversation

@Farenheith
Copy link
Copy Markdown
Member

Readonly option will auto apply Object.freeze
on cache results, and will have an logic to
avoid updating cache when nonBlocking is true
and the cache hasn't change comparing to
previous one. In this version, no deep comparison
is made, so it'll only work with object with
flat native values properties

Readonly option will auto apply Object.freeze
on cache results, and will have an logic to
avoid updating cache when nonBlocking is true
and the cache hasn't change comparing to
previous one. In this version, no deep comparison
is made, so it'll only work with object with
flat native values properties
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new resultType: 'readonly' option to Remembered to freeze cached results (via Object.freeze) and introduces a non-blocking optimization to avoid updating the cache when the new result appears unchanged.

Changes:

  • Introduces resultType configuration and types it through Remembered generics to affect return types.
  • Freezes cached results when resultType is readonly, with special handling in nonBlocking mode to reuse the cached reference when “unchanged”.
  • Updates CI workflows and adds a comprehensive unit test suite covering readonly behavior.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/remembered.ts Implements readonly freezing + nonBlocking “unchanged” reuse logic; updates method typings to reflect readonly results.
src/remembered-config.ts Adds ResultType and resultType option to the configuration type.
test/unit/remembered-readonly.spec.ts Adds tests for resultType: 'readonly' across get/getSync/wrap/nonBlocking/TTL/error scenarios.
test/unit/index.spec.ts Adjusts an existing unit test assertion.
.github/workflows/build.yml Updates GitHub Actions dependencies but leaves an invalid matrix reference.
.github/workflows/test.yml Updates GitHub Actions dependencies and simplifies the test step.
.github/workflows/lint.yml Updates GitHub Actions dependencies.
.github/workflows/semantic.yml Refactors semantic-release workflow; changes coverage upload and install approach.
bitbucket-pipelines.yml Removes the Bitbucket pipeline configuration.
Comments suppressed due to low confidence (1)

src/remembered.ts:108

  • wrap’s return type is declared as TResultType extends 'readonly' ? Readonly<R> : R, but R is a Promise. Freezing happens on the resolved value, not on the Promise object, so Readonly<R> is misleading and doesn’t reflect the actual return type. Consider typing wrap in terms of the awaited result (e.g., Promise<TResultType extends 'readonly' ? Readonly<Awaited<R>> : Awaited<R>>) so callers get correct types.
	wrap<T extends any[], K extends T, R extends Promise<any>>(
		callback: (...args: T) => R,
		getKey: (...args: K) => TKey,
		noCacheIf?: (result: R extends Promise<infer TR> ? TR : never) => boolean,
	): (...args: T) => TResultType extends 'readonly' ? Readonly<R> : R {
		return (...args: T): R => {
			const key = getKey(...(args as K));
			return this.get(key, () => callback(...args), noCacheIf) as R;
		};

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +130 to +139
if (isObj(cached) && isObj(result)) {
changed = false;
for (const prop in result) {
if (result[prop] !== cached[prop]) {
changed = true;
break;
}
}
if (!changed) return (result = cached);
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nonBlocking/readonly change-detection compares only enumerable properties from the new result (for...in result) and doesn’t verify that the cached value doesn’t have extra keys (or, for arrays, different length). This can incorrectly treat a changed value as unchanged (e.g., cached has an extra property, or cached array is longer), causing the cache to be reused when it shouldn’t. Consider comparing Object.keys on both objects (and lengths) and only comparing own-properties, or restricting this optimization to plain objects with stable prototypes.

Copilot uses AI. Check for mistakes.
export interface RememberedConfig<
TResponse = unknown,
TKey = string,
TResultType extends ResultType = 'mutable' | 'readonly',
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default for TResultType is 'mutable' | 'readonly', which makes RememberedConfig default to a union and causes conditional types to distribute (often resulting in R | Readonly<R> for callers that don’t specify the generic). Defaulting this to 'mutable' (and keeping resultType?) usually yields better inference and avoids widening return types unexpectedly.

Suggested change
TResultType extends ResultType = 'mutable' | 'readonly',
TResultType extends ResultType = 'mutable',

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +14
- uses: actions/checkout@v5
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
uses: actions/setup-node@v6
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow still references matrix.node-version in the step name, but the matrix strategy was removed. Referencing matrix.* without a matrix will fail workflow parsing/execution. Either restore the matrix strategy or remove the ${{ matrix.node-version }} reference (and optionally set an explicit node-version).

Copilot uses AI. Check for mistakes.
Comment on lines 34 to +35
- run: previousVersion=$(sed 's/.*"version"&#58; "\(.*\)".*/\1/;t;d' ./package.json)
- run: npm i -g @semantic-release/changelog @semantic-release/commit-analyzer @semantic-release/git @semantic-release/github @semantic-release/exec @semantic-release/npm @semantic-release/release-notes-generator semantic-release
- run: |
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sed pattern is looking for "version"&#58; (HTML entity) instead of the actual JSON "version":. As written, previousVersion/finalVersion will likely be empty, which breaks the conditional git push logic. Update the pattern to match the literal colon (or use node -p "require('./package.json').version" / jq -r .version).

Copilot uses AI. Check for mistakes.
semantic:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow still uses @semantic-release/npm (see .releaserc.json), but NPM_TOKEN is no longer provided anywhere. Unless you’ve switched to an alternative npm auth mechanism, semantic-release will fail when trying to publish. Either restore NPM_TOKEN (or the required env vars for npm auth) or remove/disable the npm plugin if publishing isn’t intended.

Suggested change
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Copilot uses AI. Check for mistakes.
Comment on lines 26 to +33
- run: npm ci
- run: npm run build --if-present
- name: Redis Server in GitHub Actions
uses: supercharge/redis-github-action@1.1.0
- run: npm test
- uses: actions/setup-node@v3
- name: Upload coverage to Qlty
uses: qltysh/qlty-action/coverage@v2
continue-on-error: true
with:
node-version: "lts/*"
token: ${{ secrets.CC_TEST_REPORTER_ID }}
files: coverage/lcov.info
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The release workflow no longer runs the test suite (previously npm test with Redis). Relying only on PR checks means direct pushes (or workflow_dispatch) can publish without verification. Consider running at least npm test/npm run test:coverage here (and starting Redis if needed) before invoking semantic-release.

Copilot uses AI. Check for mistakes.
Comment on lines +132 to +134
for (const prop in result) {
if (result[prop] !== cached[prop]) {
changed = true;
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

result[prop] / cached[prop] relies on indexing into values narrowed to {}; under strict this will not type-check (no index signature). If you intend this to work for plain objects, narrow to Record<string, unknown> (and ensure both sides are that type) before iterating/comparing.

Copilot uses AI. Check for mistakes.
Comment on lines +140 to +144
if (changed) result = Object.freeze(result);
}
this.nonBlockingMap.set(key, result);
} else if (isObj(result) && this.config.resultType === 'readonly') {
result = Object.freeze(result);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assigning Object.freeze(result) back into a variable typed as R is not type-safe in a generic context: Object.freeze returns Readonly<R>, which is not assignable to R when R is an unconstrained type parameter (and it’s often not assignable even for object types). This is likely a TypeScript compile error under strict. Consider widening the local variable type (e.g., R | Readonly<R>) and/or casting at the assignment boundary, and make the method return type align with the runtime behavior.

Suggested change
if (changed) result = Object.freeze(result);
}
this.nonBlockingMap.set(key, result);
} else if (isObj(result) && this.config.resultType === 'readonly') {
result = Object.freeze(result);
if (changed) result = Object.freeze(result as R) as R;
}
this.nonBlockingMap.set(key, result);
} else if (isObj(result) && this.config.resultType === 'readonly') {
result = Object.freeze(result as R) as R;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants