Skip to content

Commit 7fa8056

Browse files
authored
SyncedDiscriminatedUnion (#6)
* SyncedDiscriminatedUnion v1 * more robust implementation * imrpoved and simplified discriminated union * fixing remaining tests * fixing reviews * apply suggestions, validate two more tests * fixing out of scope stuff * Fix falsy-discriminant short-circuit and wrong type in swapValidator * test * add publication workflow
1 parent 315236b commit 7fa8056

71 files changed

Lines changed: 10055 additions & 5759 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.coderabbit.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
reviews:
2+
path_filters:
3+
# Include these paths
4+
- "package/src/**"
5+
- "package/tests/**"
6+
7+
# Exclude these paths (prefix with !)
8+
- "!node_modules/**"
9+
- "!package/node_modules/**"
10+
- "!package/dist/**"
11+
- "!package/.svelte-kit/**"
12+
- "!package/build/**"
13+
- "!package/*.min.js"
14+
- "!demo/**"

.github/workflows/README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# GitHub Actions Setup
2+
3+
This directory contains GitHub Actions workflows for automated CI/CD.
4+
5+
## NPM Publishing Setup
6+
7+
To enable automatic npm publishing, you need to set up an NPM_TOKEN secret in your GitHub repository:
8+
9+
### 1. Create NPM Access Token
10+
11+
1. Go to [npmjs.com](https://www.npmjs.com) and log in
12+
2. Click on your profile → "Access Tokens"
13+
3. Click "Generate New Token" → "Granular Access Token"
14+
4. Configure the token:
15+
- **Token name**: `syncrostate-github-actions`
16+
- **Expiration**: Set to your preference (e.g., 1 year)
17+
- **Packages and scopes**: Select your `syncrostate` package
18+
- **Permissions**:
19+
- `Read and write` for the package
20+
- `Read` for organization (if applicable)
21+
22+
### 2. Add Secret to GitHub Repository
23+
24+
1. Go to your GitHub repository
25+
2. Navigate to **Settings****Secrets and variables****Actions**
26+
3. Click **"New repository secret"**
27+
4. Name: `NPM_TOKEN`
28+
5. Value: Paste the token you created from npm
29+
6. Click **"Add secret"**
30+
31+
### 3. How the Workflow Works
32+
33+
The `publish.yml` workflow will:
34+
35+
- **Trigger**: Automatically run when code is pushed to `master` or `main` branch
36+
- **Version Check**: Compare current package version with published version on npm
37+
- **Build & Test**: Run tests and build the package
38+
- **Publish**: Only publish if the version number has changed
39+
- **Release**: Create a GitHub release with the new version tag
40+
41+
### 4. Manual Publishing
42+
43+
You can also trigger the workflow manually:
44+
1. Go to **Actions** tab in your GitHub repository
45+
2. Select "Publish to NPM" workflow
46+
3. Click "Run workflow" button
47+
48+
### 5. Version Management
49+
50+
To publish a new version:
51+
1. Update the version in `package/package.json`
52+
2. Update the `CHANGELOG.md` with new changes
53+
3. Commit and push to master/main branch
54+
4. The workflow will automatically detect the version change and publish
55+
56+
### Security Notes
57+
58+
- The NPM_TOKEN is stored securely as a GitHub secret
59+
- The workflow only publishes when the version number changes
60+
- Tests must pass before publishing
61+
- The workflow runs in an isolated environment

.github/workflows/publish.yml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: Publish to NPM
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
- main
8+
workflow_dispatch: # Allow manual triggering
9+
10+
jobs:
11+
publish:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v4
16+
17+
- name: Setup Node.js
18+
uses: actions/setup-node@v4
19+
with:
20+
node-version: "18"
21+
registry-url: "https://registry.npmjs.org"
22+
23+
- name: Setup pnpm
24+
uses: pnpm/action-setup@v2
25+
with:
26+
version: latest
27+
28+
- name: Install dependencies
29+
run: pnpm install --frozen-lockfile
30+
31+
- name: Run tests
32+
run: pnpm --filter syncrostate test
33+
34+
- name: Build package
35+
run: pnpm run package
36+
37+
- name: Check if version changed
38+
id: version-check
39+
run: |
40+
CURRENT_VERSION=$(node -p "require('./package/package.json').version")
41+
PUBLISHED_VERSION=$(npm view syncrostate version 2>/dev/null || echo "0.0.0")
42+
echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT
43+
echo "published=$PUBLISHED_VERSION" >> $GITHUB_OUTPUT
44+
if [ "$CURRENT_VERSION" != "$PUBLISHED_VERSION" ]; then
45+
echo "changed=true" >> $GITHUB_OUTPUT
46+
echo "Version changed from $PUBLISHED_VERSION to $CURRENT_VERSION"
47+
else
48+
echo "changed=false" >> $GITHUB_OUTPUT
49+
echo "Version unchanged: $CURRENT_VERSION"
50+
fi
51+
52+
- name: Publish to NPM
53+
if: steps.version-check.outputs.changed == 'true'
54+
run: |
55+
cd package
56+
npm publish
57+
env:
58+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
59+
60+
- name: Create GitHub Release
61+
if: steps.version-check.outputs.changed == 'true'
62+
uses: actions/create-release@v1
63+
env:
64+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
65+
with:
66+
tag_name: v${{ steps.version-check.outputs.current }}
67+
release_name: Release v${{ steps.version-check.outputs.current }}
68+
body: |
69+
## Changes in v${{ steps.version-check.outputs.current }}
70+
71+
See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/master/CHANGELOG.md) for detailed changes.
72+
draft: false
73+
prerelease: false

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ node_modules
33
# Output
44
.output
55
.vercel
6-
**/.svelte-kit
6+
package/.svelte-kit
7+
package/coverage
8+
demo/.svelte-kit
79
/build
810
/dist
911

CHANGELOG.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Changelog
2+
3+
## [0.0.7] - 2025-08-27
4+
5+
### Added
6+
7+
#### Literal Schema Type
8+
9+
- **New `y.literal()` schema validator** for exact value matching
10+
- Supports string, number, and boolean literal values
11+
- Perfect for discriminant keys and constant values
12+
- Includes full nullability and optionality support
13+
14+
**Example:**
15+
16+
```typescript
17+
import { y } from "syncrostate";
18+
19+
// Create literal validators
20+
const statusSuccess = y.literal("success");
21+
const statusError = y.literal("error");
22+
const maxRetries = y.literal(3);
23+
const isEnabled = y.literal(true);
24+
25+
// Usage in validation
26+
statusSuccess.isValid("success"); // true
27+
statusSuccess.isValid("error"); // false
28+
maxRetries.isValid(3); // true
29+
maxRetries.isValid(5); // false
30+
```
31+
32+
#### Discriminated Union Schema Type
33+
34+
- **New `y.discriminatedUnion()` schema validator** for type-safe union types
35+
- Automatically switches between object variants based on discriminant key
36+
- Full reactive support with automatic variant swapping
37+
- Seamless integration with Yjs for collaborative editing
38+
39+
**Example:**
40+
41+
```typescript
42+
import { y, syncroState } from "syncrostate";
43+
44+
// Define API response types
45+
const apiResponse = y.discriminatedUnion("status", [
46+
y.object({
47+
status: y.literal("success"),
48+
data: y.string(),
49+
timestamp: y.number(),
50+
}),
51+
y.object({
52+
status: y.literal("error"),
53+
message: y.string(),
54+
code: y.number(),
55+
}),
56+
y.object({
57+
status: y.literal("loading"),
58+
progress: y.number(),
59+
}),
60+
]);
61+
62+
// Use in reactive state
63+
const response = syncroState(apiResponse, {
64+
status: "loading",
65+
progress: 0,
66+
});
67+
68+
// Type-safe updates - automatically switches variants
69+
response.value = {
70+
status: "success",
71+
data: "Hello World",
72+
timestamp: Date.now(),
73+
};
74+
75+
// TypeScript knows the exact shape based on discriminant
76+
if (response.value.status === "success") {
77+
console.log(response.value.data); // ✅ TypeScript knows this exists
78+
// console.log(response.value.message); // ❌ TypeScript error
79+
}
80+
```
81+
82+
**Advanced Example - User Permissions:**
83+
84+
```typescript
85+
const userPermission = y.discriminatedUnion("role", [
86+
y.object({
87+
role: y.literal("admin"),
88+
permissions: y.array(y.string()),
89+
canDeleteUsers: y.boolean(),
90+
}),
91+
y.object({
92+
role: y.literal("user"),
93+
permissions: y.array(y.string()),
94+
}),
95+
y.object({
96+
role: y.literal("guest"),
97+
expiresAt: y.date(),
98+
}),
99+
]);
100+
101+
const user = syncroState(userPermission, {
102+
role: "guest",
103+
expiresAt: new Date(),
104+
});
105+
106+
// Upgrade user to admin
107+
user.value = {
108+
role: "admin",
109+
permissions: ["read", "write", "delete"],
110+
canDeleteUsers: true,
111+
};
112+
```
113+
114+
### Features
115+
116+
- Both literal and discriminated union types support `.optional()` and `.nullable()` modifiers
117+
- Full TypeScript type inference and safety
118+
- Seamless integration with existing syncrostate reactive system
119+
- Automatic Yjs synchronization for collaborative applications
120+
- Comprehensive validation and coercion support
121+
122+
### Technical Details
123+
124+
- Discriminated unions automatically detect variant changes and swap underlying object validators
125+
- Literal validators provide exact value matching with efficient comparison
126+
- Both types integrate with the existing proxy system for reactive updates
127+
- Full test coverage for all new functionality
128+
129+
## [0.0.6] - Previous Release
130+
131+
- Previous features and improvements undocumented

0 commit comments

Comments
 (0)