Giter VIP home page Giter VIP logo

Comments (9)

lastchiliarch avatar lastchiliarch commented on May 28, 2024 1

@lastchiliarch does my above proposal help?

Thanks for your help and hard work. It's diffcult for the "validate supervisor result" task to control the flow, seems message event could be a more graceful approach.

from lib-bpmn-engine.

nitram509 avatar nitram509 commented on May 28, 2024

Hi @lastchiliarch
if I got you right, then you want to go backward, like in a loop?
I mean basically, what does this loop BPMN shown below?

That is likely not supported in the current version ... but I will think about, how to implement it 👍

Thank you for your feedback.
Also, PRs are always welcome :)

Screenshot 2022-08-29 at 14 47 04

from lib-bpmn-engine.

lastchiliarch avatar lastchiliarch commented on May 28, 2024

Yes, that's what i want. I'm new to bpmn so the implement is diffcult for me . But I'd like to help doing a part of job of this one or anything I can.

from lib-bpmn-engine.

nitram509 avatar nitram509 commented on May 28, 2024

Hi @lastchiliarch

I've implemented a test, that show "backwards" works in this simple loop example above.
See

func Test_simple_count_loop(t *testing.T) {

from lib-bpmn-engine.

nitram509 avatar nitram509 commented on May 28, 2024

I re-read your initial post and I have to admit, I don't understand your business requirement.
What I can say, BPMN does not support "reset" == if you design some condition after the "confirm receipt",
the bpmn flow will continue a second round.

This is likeley not what you want in business, I guess.
I would rather suggest, to model a separate BPMN process for refunding.

A typical refind process could look like:

  1. find existing order process
  2. cancel existing order process (lib-bpmn-engine does not support cancelling yet, but BPMN itself is desgined for that)
  3. validate refund conditions
  4. subtract refund fees
  5. make payment order, so the user gets its money back

The benefit of having two processes are

  • more transparency, what was happen, when you "link" the processes by their keys
  • easier to understand
  • easier to distribute

How does this resonate with you?

from lib-bpmn-engine.

lastchiliarch avatar lastchiliarch commented on May 28, 2024

re-read your initial post and I have to admit, I don't understand your business requirement.
What I can say, BPMN does not support "reset" == if you design some condition after the "confirm receipt",
the bpmn flow will continue a second round.

Yes, I want reset function.

Some of my workflow likes below.
image

  1. user try to apply some auth
  2. reviewer audit the application and tag some label like city
  3. supervisor make the final confirm
  4. if supervisor think some label is not correct , he will ask the reviewer to re-audit it.
  5. reviewr re-aduit the tag updated label
  6. supervisor check the applcation again like step 4
  7. approve the auth if everything goes well

Since the enginer does not support reset, it will be traped into infinite loop if I don't clear the op variable in notifySupervisor.
So What I want is clearing the processInfo where event is trigged in backwards case.
If bpmn is not designed to support this, I'd like to have some functiones to clear or delete the processInfo variables manually to archive this goal.

Some is some of my test code and bpmn file.

var (
	bpmnEngine = New("auth_apply")
	process *ProcessInfo
	instance *ProcessInstanceInfo
)

func Test_apply_auth_loop(t *testing.T) {
	// setup
	var err error
	process, err = bpmnEngine.LoadFromFile("../../test-cases/auth_apply.bpmn")
	bpmnEngine.AddTaskHandler("apply_auth", applyAuth)
	bpmnEngine.AddTaskHandler("audit", audit)
	bpmnEngine.AddTaskHandler("prove_auth", proveAuth)
	bpmnEngine.AddTaskHandler("notify_supervisor", notifySupervisor)

	instance, err = bpmnEngine.CreateInstance(process.ProcessKey, nil)
	fmt.Println(err)
	bpmnEngine.RunOrContinueInstance(instance.GetInstanceKey())
	then.AssertThat(t, instance.GetVariable("city"), is.EqualTo("Beijing"))
	then.AssertThat(t, instance.state, is.EqualTo(process_instance.COMPLETED))
}

func applyAuth(job ActivatedJob)  {
	log.Println("start to applyAuth")
	job.Complete()
}

func audit(job ActivatedJob)  {
	log.Println("start to audit")
	op := job.GetVariable("op")
	if op == nil {
		job.SetVariable("city", "London")
		bpmnEngine.PublishEventForInstance(instance.GetInstanceKey(), "audit_by_reviewer")
		job.Complete()
		return
	}
	auditop := op.(string)
	if auditop != "" {
		job.SetVariable("city", "Beijing")
		bpmnEngine.PublishEventForInstance(instance.GetInstanceKey(), "audit_by_reviewer")
		job.Complete()
	} else {
		job.SetVariable("city", "London")
		bpmnEngine.PublishEventForInstance(instance.GetInstanceKey(), "audit_by_reviewer")
		job.Complete()
	}
}

func proveAuth(job ActivatedJob)  {
	log.Println("start to proveAuth")
	job.Complete()
}

func notifySupervisor(job ActivatedJob)  {
	log.Println("start to notifySupervisor")
	city := job.GetVariable("city").(string)
	if city != "Beijing" {
		job.SetVariable("op", "re-audit")
	} else {
		job.SetVariable("op", "")
	}
	bpmnEngine.PublishEventForInstance(instance.GetInstanceKey(), "confirm_by_supervisor")
	job.Complete()
}
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1l2k2zy" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.2.0" modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.0.0">
  <bpmn:process id="Process_16ue9kj" isExecutable="true">
    <bpmn:startEvent id="StartEvent_1">
      <bpmn:outgoing>Flow_0fwz2ln</bpmn:outgoing>
    </bpmn:startEvent>
    <bpmn:serviceTask id="prove_auth" name="prove_auth">
      <bpmn:extensionElements>
        <zeebe:taskDefinition type="prove_auth" />
      </bpmn:extensionElements>
      <bpmn:incoming>Flow_0bqwngy</bpmn:incoming>
      <bpmn:outgoing>Flow_0l4cgrv</bpmn:outgoing>
    </bpmn:serviceTask>
    <bpmn:endEvent id="Event_0jv5wxq">
      <bpmn:incoming>Flow_0l4cgrv</bpmn:incoming>
    </bpmn:endEvent>
    <bpmn:sequenceFlow id="Flow_0bqwngy" sourceRef="Gateway_1dmvvpa" targetRef="prove_auth" />
    <bpmn:serviceTask id="apply_auth" name="apply_auth">
      <bpmn:extensionElements>
        <zeebe:taskDefinition type="apply_auth" />
      </bpmn:extensionElements>
      <bpmn:incoming>Flow_0fwz2ln</bpmn:incoming>
      <bpmn:outgoing>Flow_1lr5ur1</bpmn:outgoing>
    </bpmn:serviceTask>
    <bpmn:intermediateCatchEvent id="confirm_by_supervisor" name="confirm_by_supervisor">
      <bpmn:incoming>Flow_177ps1y</bpmn:incoming>
      <bpmn:outgoing>Flow_0wvvvjc</bpmn:outgoing>
      <bpmn:messageEventDefinition id="MessageEventDefinition_1mr6cvo" messageRef="Message_1agfbps" />
    </bpmn:intermediateCatchEvent>
    <bpmn:intermediateCatchEvent id="audit_by_reviewer" name="audit_by_reviewer">
      <bpmn:incoming>Flow_1grwq17</bpmn:incoming>
      <bpmn:outgoing>Flow_11yboz4</bpmn:outgoing>
      <bpmn:messageEventDefinition id="MessageEventDefinition_0j6i8wh" messageRef="Message_27ugmpv" />
    </bpmn:intermediateCatchEvent>
    <bpmn:sequenceFlow id="Flow_0wvvvjc" sourceRef="confirm_by_supervisor" targetRef="Gateway_1dmvvpa" />
    <bpmn:parallelGateway id="Gateway_07ivcgb">
      <bpmn:incoming>Flow_11yboz4</bpmn:incoming>
      <bpmn:outgoing>Flow_06la618</bpmn:outgoing>
    </bpmn:parallelGateway>
    <bpmn:sequenceFlow id="Flow_11yboz4" sourceRef="audit_by_reviewer" targetRef="Gateway_07ivcgb" />
    <bpmn:eventBasedGateway id="Gateway_1t2zx79">
      <bpmn:incoming>Flow_0e397yl</bpmn:incoming>
      <bpmn:outgoing>Flow_1grwq17</bpmn:outgoing>
    </bpmn:eventBasedGateway>
    <bpmn:sequenceFlow id="Flow_1grwq17" sourceRef="Gateway_1t2zx79" targetRef="audit_by_reviewer" />
    <bpmn:serviceTask id="notify_supervisor" name="notify_supervisor">
      <bpmn:extensionElements>
        <zeebe:taskDefinition type="notify_supervisor" />
      </bpmn:extensionElements>
      <bpmn:incoming>Flow_06la618</bpmn:incoming>
      <bpmn:outgoing>Flow_0c8hcd4</bpmn:outgoing>
    </bpmn:serviceTask>
    <bpmn:sequenceFlow id="Flow_06la618" sourceRef="Gateway_07ivcgb" targetRef="notify_supervisor" />
    <bpmn:eventBasedGateway id="Gateway_00r4evy">
      <bpmn:incoming>Flow_0c8hcd4</bpmn:incoming>
      <bpmn:outgoing>Flow_177ps1y</bpmn:outgoing>
    </bpmn:eventBasedGateway>
    <bpmn:sequenceFlow id="Flow_0c8hcd4" sourceRef="notify_supervisor" targetRef="Gateway_00r4evy" />
    <bpmn:sequenceFlow id="Flow_177ps1y" sourceRef="Gateway_00r4evy" targetRef="confirm_by_supervisor" />
    <bpmn:sequenceFlow id="Flow_0l4cgrv" sourceRef="prove_auth" targetRef="Event_0jv5wxq" />
    <bpmn:sequenceFlow id="Flow_1lr5ur1" sourceRef="apply_auth" targetRef="audit" />
    <bpmn:sequenceFlow id="Flow_0e397yl" sourceRef="audit" targetRef="Gateway_1t2zx79" />
    <bpmn:sequenceFlow id="re_audit" name="op=&#39;re-audit&#34;" sourceRef="Gateway_1dmvvpa" targetRef="audit">
      <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=op matches "re-audit"</bpmn:conditionExpression>
    </bpmn:sequenceFlow>
    <bpmn:serviceTask id="audit" name="audit">
      <bpmn:extensionElements>
        <zeebe:taskDefinition type="audit" />
      </bpmn:extensionElements>
      <bpmn:incoming>Flow_1lr5ur1</bpmn:incoming>
      <bpmn:incoming>re_audit</bpmn:incoming>
      <bpmn:outgoing>Flow_0e397yl</bpmn:outgoing>
    </bpmn:serviceTask>
    <bpmn:sequenceFlow id="Flow_0fwz2ln" sourceRef="StartEvent_1" targetRef="apply_auth" />
    <bpmn:exclusiveGateway id="Gateway_1dmvvpa">
      <bpmn:incoming>Flow_0wvvvjc</bpmn:incoming>
      <bpmn:outgoing>Flow_0bqwngy</bpmn:outgoing>
      <bpmn:outgoing>re_audit</bpmn:outgoing>
    </bpmn:exclusiveGateway>
  </bpmn:process>
  <bpmn:message id="Message_27ugmpv" name="audit_by_reviewer">
    <bpmn:extensionElements>
      <zeebe:subscription correlationKey="=key" />
    </bpmn:extensionElements>
  </bpmn:message>
  <bpmn:message id="Message_067eof3" name="Message_067eof3">
    <bpmn:extensionElements>
      <zeebe:subscription correlationKey="=key" />
    </bpmn:extensionElements>
  </bpmn:message>
  <bpmn:message id="Message_1agfbps" name="confirm_by_supervisor">
    <bpmn:extensionElements>
      <zeebe:subscription correlationKey="=key" />
    </bpmn:extensionElements>
  </bpmn:message>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_16ue9kj">
      <bpmndi:BPMNEdge id="Flow_0fwz2ln_di" bpmnElement="Flow_0fwz2ln">
        <di:waypoint x="168" y="177" />
        <di:waypoint x="200" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_199ps6v_di" bpmnElement="re_audit">
        <di:waypoint x="1000" y="152" />
        <di:waypoint x="1000" y="90" />
        <di:waypoint x="390" y="90" />
        <di:waypoint x="390" y="137" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="665" y="72" width="63" height="14" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0e397yl_di" bpmnElement="Flow_0e397yl">
        <di:waypoint x="440" y="177" />
        <di:waypoint x="475" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_1lr5ur1_di" bpmnElement="Flow_1lr5ur1">
        <di:waypoint x="300" y="177" />
        <di:waypoint x="340" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0l4cgrv_di" bpmnElement="Flow_0l4cgrv">
        <di:waypoint x="1190" y="177" />
        <di:waypoint x="1272" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_177ps1y_di" bpmnElement="Flow_177ps1y">
        <di:waypoint x="865" y="177" />
        <di:waypoint x="892" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0c8hcd4_di" bpmnElement="Flow_0c8hcd4">
        <di:waypoint x="790" y="177" />
        <di:waypoint x="815" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_06la618_di" bpmnElement="Flow_06la618">
        <di:waypoint x="665" y="177" />
        <di:waypoint x="690" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_1grwq17_di" bpmnElement="Flow_1grwq17">
        <di:waypoint x="525" y="177" />
        <di:waypoint x="552" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_11yboz4_di" bpmnElement="Flow_11yboz4">
        <di:waypoint x="588" y="177" />
        <di:waypoint x="615" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0wvvvjc_di" bpmnElement="Flow_0wvvvjc">
        <di:waypoint x="928" y="177" />
        <di:waypoint x="975" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0bqwngy_di" bpmnElement="Flow_0bqwngy">
        <di:waypoint x="1025" y="177" />
        <di:waypoint x="1090" y="177" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
        <dc:Bounds x="132" y="159" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_0d43kkb_di" bpmnElement="apply_auth">
        <dc:Bounds x="200" y="137" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_05mf2rz_di" bpmnElement="confirm_by_supervisor">
        <dc:Bounds x="892" y="159" width="36" height="36" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="865" y="121.5" width="89" height="27" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_001ghjo_di" bpmnElement="audit_by_reviewer">
        <dc:Bounds x="552" y="159" width="36" height="36" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="527" y="202" width="86" height="27" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Gateway_1mfbkhy_di" bpmnElement="Gateway_07ivcgb">
        <dc:Bounds x="615" y="152" width="50" height="50" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Gateway_1u2qqcb_di" bpmnElement="Gateway_1t2zx79">
        <dc:Bounds x="475" y="152" width="50" height="50" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_1be98t1_di" bpmnElement="notify_supervisor">
        <dc:Bounds x="690" y="137" width="100" height="80" />
        <bpmndi:BPMNLabel />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Gateway_1kgw3tw_di" bpmnElement="Gateway_00r4evy">
        <dc:Bounds x="815" y="152" width="50" height="50" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_1doq7r3_di" bpmnElement="audit">
        <dc:Bounds x="340" y="137" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_0jv5wxq_di" bpmnElement="Event_0jv5wxq">
        <dc:Bounds x="1272" y="159" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_0zsfe4z_di" bpmnElement="prove_auth">
        <dc:Bounds x="1090" y="137" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Gateway_0vfgvoa_di" bpmnElement="Gateway_1dmvvpa" isMarkerVisible="true">
        <dc:Bounds x="975" y="152" width="50" height="50" />
      </bpmndi:BPMNShape>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn:definitions>

from lib-bpmn-engine.

nitram509 avatar nitram509 commented on May 28, 2024

Got your point ... there seems to be another bug, working on it.

from lib-bpmn-engine.

nitram509 avatar nitram509 commented on May 28, 2024

Hi siconghuang,

I did just a minute ago post some fixes, which were still present, for the bug mentioned above.

I did some modifications as well, to make the code + BPMN more simple.

  • IF there's just a single intermediate message catch event in the flow, you don't need a event gateway in front neither an exclusive gateway afterward
  • you should not send events from inside a service task
    • that might be technically possible, but is not necessary code, the engine does the process execution for you
    • it's very difficult to read/understand such code
  • from your example, I think it would be more convenient to have the #41 implemented, BUT as you can see, the "validate supervisor result" task takes care of updating the proper variable within the instance.

Screenshot 2022-09-07 at 21 41 55

import (
	"fmt"
	"github.com/corbym/gocrest/is"
	"github.com/corbym/gocrest/then"
	"github.com/nitram509/lib-bpmn-engine/pkg/bpmn_engine"
	"github.com/nitram509/lib-bpmn-engine/pkg/spec/BPMN20/process_instance"
	"log"
	"testing"
)

var (
	bpmnEngine = bpmn_engine.New("auth_apply")
	process    *bpmn_engine.ProcessInfo
	instance   *bpmn_engine.ProcessInstanceInfo
)

// these test-* variables do mimic/simulate external events or manual actions
var testAuditAttempt = 0

const BEIJING = "Beijing"
const LONDON = "London"

func Test_apply_auth_loop(t *testing.T) {
	// setup
	var err error
	process, err = bpmnEngine.LoadFromFile("auth_apply.bpmn")
	bpmnEngine.AddTaskHandler("apply_auth", applyAuth)
	bpmnEngine.AddTaskHandler("audit", audit)
	bpmnEngine.AddTaskHandler("notify_supervisor", notifySupervisor)
	bpmnEngine.AddTaskHandler("validate_supervisor_result", validateSupervisorResult)
	bpmnEngine.AddTaskHandler("prove_auth", proveAuth)

	instance, err = bpmnEngine.CreateInstance(process.ProcessKey, nil)
	fmt.Println(err)

	// HINT: BPMN spec allows message published with variables, but the engine does not (yet) support that
	// so ideally, bpmnEngine.PublishEventForInstance() should support variables to pass in

	bpmnEngine.RunOrContinueInstance(instance.GetInstanceKey())
	then.AssertThat(t, instance.GetVariable("city"), is.EqualTo(LONDON))

	bpmnEngine.PublishEventForInstance(instance.GetInstanceKey(), "supervision_reported") // first response from supervisor
	bpmnEngine.RunOrContinueInstance(instance.GetInstanceKey())                           // will loop back and executes the audit as well, stops because of second intermediate catch event

	bpmnEngine.PublishEventForInstance(instance.GetInstanceKey(), "supervision_reported") // first response from supervisor
	bpmnEngine.RunOrContinueInstance(instance.GetInstanceKey())                           // will catch up the second event and exits the instance
	then.AssertThat(t, instance.GetVariable("city"), is.EqualTo(BEIJING))

	then.AssertThat(t, instance.GetState(), is.EqualTo(process_instance.COMPLETED))
}

func validateSupervisorResult(job bpmn_engine.ActivatedJob) {
	reAudit := testAuditAttempt <= 1 // for testing purpose, the first attempt is always wrong
	log.Println(fmt.Sprintf("validateSupervisorResult, reAudit=%t", reAudit))
	job.SetVariable("reAudit", reAudit)
	job.Complete()
}

func applyAuth(job bpmn_engine.ActivatedJob) {
	log.Println("start to applyAuth")
	job.Complete()
}

func audit(job bpmn_engine.ActivatedJob) {
	log.Println("start to audit")
	if testAuditAttempt < 1 {
		job.SetVariable("city", LONDON)
	} else {
		job.SetVariable("city", BEIJING)
	}
	testAuditAttempt = testAuditAttempt + 1 // change audit result for next time
	job.Complete()
}

func proveAuth(job bpmn_engine.ActivatedJob) {
	log.Println("start to proveAuth")
	job.Complete()
}

func notifySupervisor(job bpmn_engine.ActivatedJob) {
	city := job.GetVariable("city").(string)
	log.Println(fmt.Sprintf("start to notifySupervisor; city=%s", city))
	job.Complete()
}

BPMN Source ...
auth_apply.bpmn.txt

from lib-bpmn-engine.

nitram509 avatar nitram509 commented on May 28, 2024

@lastchiliarch does my above proposal help?

from lib-bpmn-engine.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.